I am working on a grocery list project. With this project, when the user enters an item, I want to give the ability to edit said item. I am storing everything inside an array of objects in my state. The structure of my objects is:
{
product: 'toast',
category: 'bakery',
quantity: 3,
type: 'each
},
{
product: 'apple',
category: 'produce',
quantity: 2,
type: 'each'
},
{
product: 'hamburger',
category: 'meat',
quantity: 1,
type: 'Lb'
}
What I want to be able to do is have the user select one of those objects inside a card type function, then update it. Currently, I can add items to the list, but I can not update them.
I have tried setList(list[i].txt=v) and setList(list=>list[i].product=v) plus other variations trying to target the specific object. Any ideas would be greatly appreciated.
The following is my main app.js code. NOTE: const Change() is where I am trying to update the object. The variables that I am passing in come from my item.js code
import React ,{useState,useEffect} from 'react';
import List from './components/list';
import Header from './components/header';
function App() {
const [list, setList] = useState([]);
const Add = (p, c, q, t, crt) => {
console.log({product: p, category: c, quantity: q, type: t, cart: crt})
setList(list=>[...list,{product:p, category:c, quantity:q, type:t, cart: crt}])
}
const Change = (i, txt, v) => {
//setList(list[i].txt=v)
console.log('id: ' + i + ' topic: ' + txt + ' value: ' +v)
setList(list=>list[i].product=v)
}
const Delete = () => {
}
return (
<div>
{console.log(list)}
<h1>Grocery List App</h1>
<Header add={Add}/>
<List set={setList} lst={list} chg={Change} del={Delete} />
</div>
);
}
export default App;
This next code is my list.js file. I am iterating over my list state and creating the individual 'cards' for each item.
import React from 'react';
import Card from './item';
const List = (props) => {
const productChange = (txt, v) => {
console.log(props.lst[v].product)
}
const quantityChange = () => {
}
const cartChange = () => {
}
return(
<div>
<p>To Find:</p>
<ul>
{ props.lst.map((item, index) =>
item.cart === false ?
<Card
key={item.index}
index={index}
value={index}
cart={item.cart}
item={item.product}
units={item.quantity}
unitType={item.type}
cartChange={cartChange}
itemChange={productChange}
quantityChange={quantityChange}
change={props.chg}
delete={props.del}/>
: null)
}
</ul>
<p>Found</p>
<ul>
{ props.lst.map((item, index) =>
item.cart === true ?
<Card
key={item.index}
index={index}
value={index}
cart={item.cart}
item={item.product}
units={item.quantity}
unitType={item.unit}
cartChange={cartChange}
itemChange={productChange}
quantityChange={quantityChange}
change={props.chg}
delete={props.del}/>
: null)
}
</ul>
</div>
)
}
export default List;
This is the code for item.js. This is the final spot where I display the information from the list. NOTE: change() from the first file is getting called here when I change the text of an input.
import React from 'react';
const Card=(props)=>{
return (
<li key={props.value}>
<div>
<input
type="checkbox"
checked={props.cart}
onChange={(e)=> {props.cartChange(props.value)}} />
</div>
<div>
<input
id={'product '+ props.value}
className='update'
type='text'
value={props.item}
onChange={(e) =>
props.change(props.value,'product', e.target.value)
}
/>
<br/>
<input
id='quantityValue'
className='update'
type='number'
value={props.units}
// onChange={(e)=>
props.quantityChange(e.target.value, props.value)}
/>
<span id='quantityType' className='update'>{props.unitType}
</span>
</div>
<div>
<button
id='save-button'
type='button'
onClick={(e) => { props.change(
props.item,
props.units,
props.unitType,
props.value)
}
}>✓ save</button>
<button
id='delete-button'
type='button'
onClick={(e) => {props.delete(props.value)}}>✗ delete</button>
</div>
</li>
)
}
export default Card;
you want to call setList with a list where you just amend that one object.
You can use list.map() for this. Ideally you want to add ID field to your objects.
But even without ID you can use index:
setList(list.map((product, index)=>{index == i ? v : product}))
or more verbose:
const Change = (i, txt, v) =>{
const newList = list.map((product, index) => {
return index == i ? v : product
});
setList(newList);
}
You want to implement change function. Is this right?
Try this one.
const Change = (i,txt,v) =>{
setList(list.map((e, ei) => {
if (ei === i) {
e.product = v;
return e;
}
return e;
}));
}
Related
I'm new to REACT.. I'm trying todo list websites based on the video for beginner react. But some function that has been use in REACT, which is id, I cannot understand
todo listList.length === 0 ? 1 : todoList[todoList.length -1].id + 1,
this function is assigned to add tasks in add list, but I don't know how it works using this code. If someone can kindly explain this to me, it will be a great of help. I keep on repeating on this part in the video, but still, it is hard to understand. Thank you.
This is the full code for app.js
import "./App.css";
import {useState} from "react";
import { Task } from "./Task"
function App() {
const [todoList, setTodoList] = useState([]);
const [newTask, setNewTask] = useState("");
const handleChange = (event) => {
setNewTask(event.target.value);
};
const addTask = () => {
const task = {
id: todoList.length === 0 ? 1 : todoList[todoList.length -1].id + 1,
taskName: newTask,
completed: false,
};
setTodoList([...todoList, task]);
};
const deleteTask = (id) => {
setTodoList(todoList.filter((task) => task.id !== id));
};
const completeTask = (id) => {
setTodoList(
todoList.map((task)=> {
if(task.id === id) {
return { ...task, completed: true };
} else {
return task;
}
})
);
};
return (
<div className="App">
<div className="addTask">
<input onChange={handleChange} />
<button onClick={addTask}> Add task</button>
</div>
<div cl assName="list">
{todoList.map((task) => {
return (
<Task
taskName={task.taskName}
id={task.id}
completed={task.completed}
deleteTask={deleteTask}
completeTask={completeTask}
/>
);
})}
</div>
</div>
);
}
export default App;
This is for task.js
export const Task = (props) => {
return (
<div
className="task"
style={{ backgroundColor: props.completed ? "pink" : "white"}}
>
<h1>{props.taskName}</h1>
<button Onclick={() => props.completeTask(props.id)}> Complete </button>
<button onClick={() => props.deleteTask(props.id)}> X </button>
</div>
);
}
Okay, it's really simple. You need to generate a special id for every task. one of the easiest ways you can do that is to generate an id from 1 to how many you want or need. here todoList.length === 0 ? 1 : todoList[todoList.length -1].id + 1 you first check if there is any task assigned or not. if not you generate the first id and it's 1. from here each task you add will get the last task id and add 1 number to it. so by this definition, the first task will have 1 as id and the second will have 2 as id.
I'm pretty new to react and I'm trying to update a list from a child component based on user input add it will not update correctly. The idea is the user is able to add multiple different bikes in one form, each with a type and an ageRange property. When the user clicks an "Add Bike" button it adds BikeInput component (which works fine) and is supposed to be adding a new empty bike to a list of bikes that will be sent when the form is submitted. I console logged in a useEffect function the list of bikes after adding a new input and that works okay, but when I try to set one of the new bikes it removes all the elements from the list except the first. Again I'm pretty new to react so I'm not exactly sure if I'm using the useEffect function correctly or if there's another way to go about this, but if you could let me know that'd be amazing.
Here's some snippets of the important parts of the code that relate to the type property since the ageRange should work the same way
Parent Component:
import { useState, useEffect } from 'react'
const initialList = {
"id": 0,
"type": "",
"ageRange": ""
};
function Donate() {
const [bikes, setBikes] = useState([initialList]);
useEffect(() => console.log(bikes), [bikes])
const setBikeType = (bikeType, bikeIndex) => {
const updateBikes = bikes.map((bike) => {
if (bike.id == bikeIndex) {
bike.type = bikeType;
}
return bike;
});
setBikes(updateBikes);
}
const [bikeInputs, setBikeInputs] = useState([
<BikeInput
setBikeType={setBikeType}
setBikeAgeRange={setBikeAgeRange}
bikeIndex={0} />]);
const addBikeForm = (e) => {
e.preventDefault();
var newBikeIndex = bikeInputs.length;
setBikeInputs(bikeInputs =>
[...bikeInputs,
<BikeInput
setBikeType={setBikeType}
setBikeAgeRange={setBikeAgeRange}
bikeIndex={newBikeIndex}
/>
]
);
var newBikeId = bikes[bikes.length - 1].id + 1;
setBikes(bikes => [...bikes, { "id": newBikeId, "type": "", "ageRange": "" }]);
};
return (
<div className="bike-form-container donate-form-one">
...
<p className="input-title">Bike Info.</p>
{bikeInputs.map((item, i) => (
<div className="bike-input" key={i}>{item}</div>
))}
<button className="add-bike-btn" onClick={addBikeForm}>
<i class="fa-solid fa-circle-plus"></i> Add A Bike
</button>
...
</div>
)
}
export default Donate
Child Component (BikeInput):
function BikeInput(props) {
return (
<div className="input-container">
<select className="form-dropdown text-input"
defaultValue="Type"
onChange={e => props.setBikeType(e.target.value, props.bikeIndex)} >
<option disabled>Type</option>
<option value="Mountain"> Mountain </option>
<option value="Road"> Road </option>
<option value="Cruiser"> Cruiser </option>
<option value="Hybrid"> Hybrid </option>
<option value="Three Wheel"> Three Wheel (Tricycle) </option>
</select>
...
</div>
)
}
export default BikeInput
Remove your bikeInputs state, since you don't have to keep a collection of BikeInputs components. Just use the BikeInput component inside bikes.map to render each bike select option.
Please simplify and update your Donate component code as follows:
export function Donate() {
const [bikes, setBikes] = useState([initialList]);
useEffect(() => console.log(bikes), [bikes]);
const setBikeType = useCallback(
(bikeType, bikeIndex) => {
const updateBikes = bikes.map((bike) => {
if (bike.id === bikeIndex) {
bike.type = bikeType;
}
return bike;
});
setBikes(updateBikes);
},
[bikes]
);
const addBikeForm = useCallback(
(e) => {
e.preventDefault();
setBikes((bikes) => {
const newBikeId = bikes[bikes.length - 1].id + 1;
return [...bikes, { id: newBikeId, type: "", ageRange: "" }];
});
},
[setBikes]
);
return (
<div className="bike-form-container donate-form-one">
<p className="input-title">Bike Info.</p>
{bikes.map((item, i) => (
<div className="bike-input" key={i}>
<BikeInput bikeIndex={i} setBikeType={setBikeType} />
</div>
))}
<button className="add-bike-btn" onClick={addBikeForm}>
<i className="fa-solid fa-circle-plus"></i> Add A Bike
</button>
</div>
);
}
could you please help me,
I add item by clicking "On" button but instead of adding 1 item it add 2 items at once, video: https://www.loom.com/share/2f9d0b48817b4480934de63a781ed6a9
Could you please help why and how to add just 1 item?
StepForm.js:
import React from 'react';
import { useState } from "react";
import OneDay from "./OneDay";
function StepsForm () {
const [form, setForm] = useState ({
date: '',
distance: ''
});
const [dataList, setList] = useState([
{id: '1613847600000', date: '21.01.2021', distance: '3.8'},
{id: '1624579200000', date: '25.05.2021', distance: '5.7'},
{id: '1642723200000', date: '21.12.2021', distance: '1.8'}
]);
const handleChange = ({target}) => {
const name = target.name;
const value = target.value;
setForm(prevForm => ({...prevForm, [name]: value}));
}
function dateValue (data) {
const year = data.substring(6,10);
const month = data.substring(3,5);
const day = data.substring(0,2);
const id = new Date(year, month, day).valueOf();
return `${id}`;
}
const handleSubmit = evt => {
evt.preventDefault();
const newData = {
id: dateValue (form.date),
date: form.date,
distance: form.distance
}
const index = dataList.findIndex((item) => item.id === newData.id);
setList(prevList => {
if (index === -1) {
prevList.push(newData);
prevList
.sort((a, b) => {return a.id - b.id})
.reverse();
} else {
prevList[index].distance = String(prevList[index].distance * 1 + newData.distance * 1)
}
return [...prevList];
})
setForm({date: '', distance: ''});
}
const clickToDelete = (evt) => {
const index = dataList.findIndex((item) => item.id === evt.target.dataset.id);
if (index === -1) {
console.log('Что-то пошло не так')
return;
}
setList(prevList => {
prevList.splice(index, 1);
return [...prevList];
})
}
return (
<div className="box">
<form onSubmit={handleSubmit}>
<div className="formBox">
<div className="inputBox">
<label htmlFor="date">Дата (ДД.ММ.ГГ)</label>
<input id="date" name="date" onChange={handleChange} value={form.date} />
</div>
<div className="inputBox">
<label htmlFor="distance">Пройдено км</label>
<input id="distance" name="distance" onChange={handleChange} value={form.distance} />
</div>
<button className="btn" type="submit">Ok</button>
</div>
</form>
<div className="headings">
<div className="heading">Дата (ДД.ММ.ГГ)</div>
<div className="heading">Пройдено км</div>
<div className="heading">Действия</div>
</div>
<div className="table">
{dataList.map(
o => <OneDay key={o.id} item={o} delDay={clickToDelete} />
)}
</div>
</div>
)
}
export default StepsForm;
App.js:
import React from 'react';
import StepsForm from './StepsForm';
import './style.css';
export default function App() {
return (
<div className="App">
<StepsForm />
</div>
);
}
export default App;
I add item by clicking "On" button but instead of adding 1 item it add 2 items at once, video: https://www.loom.com/share/2f9d0b48817b4480934de63a781ed6a9
Could you please help why and how to add just 1 item?
adding elements into the array seems working.
but, there might be an issue with dateValue(data) as it can returns NaN as an id for the element which might cause a problem with the rendering element on the screen.
function dateValue(data) {
const year = data.substring(6, 10);
const month = data.substring(3, 5);
const day = data.substring(0, 2);
const id = new Date(year, month, day).valueOf();
return `${id}`; // this return id as NaN
}
// You can create Uid like code example below
function dateValue() {
const id = new Date().valueOf();
return `${id}`;
}
Hope this solves the problem.
I am using React-TypeScript to create a list of employees, edit and delete them. However my edit button does not do anything on click. Any idea on how I can fix this problem?
I have used the same code for the "Delete" function and works perfectly. Following are my two react-typescript files which I have my codes written in and I also have an app.tsx file which all of these are being imported in it.
CreateEmployee.tsx
import React, { useState, FC, ChangeEvent } from "react";
import { Link } from "react-router-dom";
import { IEmployee } from "../../Interfaces";
import EmployeeList from "./EmployeeList";
export const CreateEmployee: FC = () => {
const [employeeName, setEmployeeName] = useState<string>("");
const [employeeId, setEmployeeId] = useState<any>("");
const [employeeList, setEmployeeList] = useState<IEmployee[]>([]);
// get the data from the input and add it to the state
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
if (event.target.name === "employeename") {
setEmployeeName(event.target.value);
} else {
setEmployeeId(event.target.value);
}
};
// add id and name to the employee list
const addEmployee = (): void => {
const newEmployee = {
employeeId: employeeId,
employeeName: employeeName,
};
setEmployeeList([...employeeList, newEmployee]);
setEmployeeName("");
setEmployeeId("");
};
// delete employee from the list
const deleteEmployee = (employeeNameToDelete: string): void => {
setEmployeeList(
employeeList.filter(
(employee) => employee.employeeName !== employeeNameToDelete
)
);
};
// edit employee name and id in the list
const editEmployee = (
employeeNameToEdit: string,
employeeIdToEdit: string
): void => {
setEmployeeList(
employeeList.map((employee) => {
if (employee.employeeName === employeeNameToEdit) {
employee.employeeId = employeeIdToEdit;
}
return employee;
})
);
};
return (
<div>
<h1>Create Employee</h1>
<div>
<label>Employee Id:</label>
<input
type="text"
name="employeeid"
value={employeeId}
onChange={(e) => setEmployeeId(e.target.value)}
/>
<label>Employee Name:</label>
<input
type="text"
name="employeename"
value={employeeName}
onChange={(e) => setEmployeeName(e.target.value)}
/>
<button type="submit" onClick={addEmployee}>
Submit
</button>
</div>
<div>
<Link to="/employeelist">Employee List</Link>
<Link to="/engagementList">Engagement List</Link>
</div>
<div className="employee-list">
<h1>Employee List</h1>
{employeeList.map((employee: IEmployee, key: number) => {
return (
<EmployeeList
key={key}
employee={employee}
deleteEmployee={deleteEmployee}
editEmployee={editEmployee}
/>
);
})}
</div>
</div>
);
};
EmployeeList.tsx
import React from "react";
import { IEmployee } from "../../Interfaces";
import { CreateEmployee } from "../employeeComponents/CreateEmployee";
interface Props {
employee: IEmployee;
deleteEmployee: (employeeNameToDelete: string) => void;
editEmployee: (employeeNameToEdit: string, employeeIdToEdit: string) => void;
}
export const EmployeeList = ({ employee, deleteEmployee, editEmployee }: Props) => {
return (
<div>
<div className="employee-list">
<div className="content">
<span>{employee.employeeId}</span>
<span>{employee.employeeName}</span>
</div>
<button
onClick={() => {
deleteEmployee(employee.employeeName);
}}
>
Delete
</button>
<button
onClick={() => {
editEmployee(employee.employeeName, employee.employeeId);
}}
>
Edit
</button>
</div>
</div>
);
};
export default EmployeeList;
You are using the current state's value employeeList within its setter. Instead of passing an object to useState, you can pass a function instead that takes the old state as a parameter. For example:
setEmployeeList((oldEmployeeList) =>
(oldEmployeeList.map((employee) => {
if (employee.employeeName === employeeNameToEdit) {
employee.employeeId = employeeIdToEdit;
}
return employee;
}))
);
This is an issue of state mutation, you are mutating the employee object you are editing instead of returning a new employee object reference. Since the specific employee object reference doesn't change React is likely bailing on rerendering them since it uses shallow reference equality checks during Reconciliation.
const editEmployee = (
employeeNameToEdit: string,
employeeIdToEdit: string
): void => {
setEmployeeList(employeeList => employeeList.map((employee) => {
if (employee.employeeName === employeeNameToEdit) {
return { // <-- return new object reference
...employee, // <-- shallow copy previous state
employeeId: employeeIdToEdit, // <-- set property value
}
}
return employee; // <-- else return previous state
}));
};
The problem was not that you were mutating the state, your editEmployee function was using setEmployeeList correctly -- map returns a new array, and React was detecting this change and re-rendering the component. You can verify that by inserting console.log('rendering'); into your CreateEmployee function.
The problem was that editEmployee was just creating an exact copy of the old employeeList.
What you probably want for it to do is to populate the text fields with the edited employee id and name, and then update your employee list when the submit button is clicked.
const editEmployee = (
employeeNameToEdit: string,
employeeIdToEdit: string
): void => {
// This will put these two items into the text fields for editing
setEmployeeId(employeeIdToEdit);
setEmployeeName(employeeNameToEdit);
};
and change addEmployee to something like this (you may rename it to addOrUpdateEmployee):
const addEmployee = (): void => {
const newEmployee = {
employeeId: employeeId,
employeeName: employeeName,
};
// Consider it's a new employee if the employee with this employeeId
// does not exist in the list of employees.
// Otherwise, it's an edited employee
let employeeIndex;
if (
employeeList.some((employee, index) => {
employeeIndex = index;
return employee.employeeId === employeeId;
})
) {
// This is not state mutation ...
employeeList[employeeIndex] = newEmployee;
// ...because we set it to a copy of the mutated array
setEmployeeList([...employeeList]);
} else {
setEmployeeList([...employeeList, newEmployee]);
}
setEmployeeName('');
setEmployeeId('');
};
The logic to decide if it's a new or edited employee is entirely mine, you should use whatever is appropriate for your app.
I'm pulling countries from the Restcountries API and if the current state of the array has more than one or less than or equal to ten countries, I want to list the country names along with a 'show' button next to each one. The show button should display what's in the return (render) of my Country function. In the App function, I wrote a handler for the button named handleViewButton. I'm confused on how to filter the element in the Countries function in the else conditional statement in order to display the Country. I tried passing handleViewButton to the Button function, but I get an error 'Uncaught TypeError: newSearch.toLowerCase is not a function'. I really just want to fire the Country function to display the country button that was pressed.
App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import axios from 'axios';
const Country = ({country}) => {
return (
<>
<h2>{country.name}</h2>
<p>capital {country.capital}</p>
<p>population {country.population}</p>
<br/>
<h3>languages</h3>
{country.languages.map(language => <li key={language.name}>{language.name}</li>)}
<br/>
<img src={country.flag} alt="country flag" style={{ width: '250px'}}/>
</>
);
}
const Countries = ({countries, handleViewButton}) => {
const countriesLen = countries.length;
console.log(countriesLen)
if (countriesLen === 0) {
return (
<p>Please try another search...</p>
);
} else if (countriesLen === 1) {
return (
<ul>
{countries.map((country, i) => <Country key={i} countriesLen={countriesLen} country={country}/>)}
</ul>
);
} else if (countriesLen > 10) {
return (
<div>
<p>Too many matches, specify another filter</p>
</div>
);
} else {
return (
<ul>
{countries.map((country, i) => <li key={i}>{country.name}<Button handleViewButton={handleViewButton}/></li>)}
</ul>
)
};
};
const Button = ({handleViewButton}) => {
return (
<button onClick={handleViewButton}>Show</button>
);
};
const Input = ({newSearch, handleSearch}) => {
return (
<div>
find countries <input value={newSearch} onChange={handleSearch}/>
</div>
);
};
function App() {
const [countries, setCountries] = useState([]);
const [newSearch, setNewSearch] = useState('');
const handleSearch = (event) => {
const search = event.target.value;
setNewSearch(search);
};
const handleViewButton = (event) => {
const search = event.target.value;
setNewSearch(countries.filter(country => country === search));
};
const showCountrySearch = newSearch
? countries.filter(country => country.name.toLowerCase().includes(newSearch.toLowerCase()))
: countries;
useEffect(() => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(res => {
setCountries(res.data);
console.log('Countries array loaded');
})
.catch(error => {
console.log('Error: ', error);
})
}, []);
return (
<div>
<Input newSearch={newSearch} handleSearch={handleSearch}/>
<Countries countries={showCountrySearch} handleViewButton={handleViewButton}/>
</div>
);
};
export default App;
you can use a displayCountry to handle the country that should be displayed. Most often you would use an id, but I'm using here country.name since it should be unique.
Then you would use matchedCountry to find against your list of countries.
After that, a onHandleSelectCountry to select a given country. if it's already selected then you could set to null to unselect.
Finally, you would render conditionally your matchedCountry:
const Countries = ({countries}) => {
const [displayCountry, setDisplayCountry] = useState(null);
const countriesLen = countries.length;
const matchedCountry = countries.find(({ name }) => name === displayCountry);
const onHandleSelectCountry = (country) => {
setDisplayCountry(selected => {
return selected !== country.name ? country.name : null
})
}
if (countriesLen === 0) {
return (
<p>Please try another search...</p>
);
} else if (countriesLen === 1) {
return (
<ul>
{countries.map((country, i) => <Country key={i} countriesLen={countriesLen} country={country}/>)}
</ul>
);
} else if (countriesLen > 10) {
return (
<div>
<p>Too many matches, specify another filter</p>
</div>
);
} else {
return (
<>
<ul>
{countries.map((country, i) => <li key={i}>{country.name}<Button handleViewButton={() => onHandleSelectCountry(country)}/></li>)}
</ul>
{ matchedCountry && <Country countriesLen={countriesLen} country={matchedCountry}/> }
</>
)
};
};
I can only help to point out some guidelines.
First: The button does not have value attribute. Hence what you will get from event.target.value is always blank.
const Button = ({handleViewButton}) => {
return (
<button onClick={handleViewButton}>Show</button>
);};
First->Suggestion: Add value to the button, of course you need to pass the value in.
const Button = ({handleViewButton, value}) => {
return (
<button onClick={handleViewButton} value={value}>Show</button>
);};
Second: To your problem 'Uncaught TypeError: newSearch.toLowerCase is not a function'. Filter always returns an array, not a single value. if you do with console or some sandbox [1,2,3].filter(x=>x===2) you will get [2] not 2.
const handleViewButton = (event) => {
const search = event.target.value;
setNewSearch(countries.filter(country => country === search));
};
Second->Suggestion: To change it to get the first element in array, since country(logically) is unique.
const result = countries.filter(country => country === search)
setNewSearch(result.length>0?result[0]:"");
A better approach for array is find, which always return first result and as a value. E.g. [1,2,2,3].find(x=>x===2) you will get 2 not [2,2] or [2].
countries.find(country => country === search)