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.
Related
I am new to react and creating my first react app. not sure why the todo list is not saved even though I have used localStorage set and get methods. I am also getting error about the key in my map method. I can't seen to find any issues on my own with the code.Below is the code of the todo list App
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
setTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
setTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
import Todo from './Todo'
export default function TodoList({todo,toggleTodo}) {
return (
todo.map((todo)=> {
return <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} />
})
)
}
This:
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
Is probably taking the initial state of todos on the first render (empty array) and overwriting what data was in their with that initial state.
You might think the previous effect counters this since todos is populated from local storage -- but it doesn't, because on that initial render pass, the second effect will only see the old value of todos. This seems counter-intuitive at first. But it's because whenever you call a set state operation, it doesn't actual change the value of todos immediately, it waits until the render passes, and then it changes for the next render. I.e. it is, in a way, "queued".
For the local storage setItem, you probably want to do it in the event handler of what manipulates the todos and not in an effect. See the React docs.
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
const storeTodos = (todos) => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
setTodos(todos)
}
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
storeTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
storeTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
storeTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
As the for the key error, we'd need to see the code in TodoList, but you need to ensure when you map over them, that the id property of each todo is passed to a key prop on the top most element/component within the map callback.
I'm trying to add a tag user inputs to an existing list of tags but I keep getting TypeError. Here is a simple version of the code. I created the addTag in the App.js file and called it in the Profil.js
In the App.js
import tag from './tag.json'
const [tagList, setTagList] = useState(tag);
const addTag = (tagInput) => {
console.log("adding..")
const newTag = { id : Date.now(), tag: tagInput}
setTagList([...tagList,newTag])
console.log("added..")
}
return (
<Profile key={e.id} name={e.name} username={e.username} email={e.email} website={e.website} company={e.company.name}
street={e.address.street} suite={e.address.suite} city={e.address.city} zipcode={e.address.zipcode}
addTag={addTag} /> )
In Profile.js
const Profile = (user,{addTag}) => {
const [tagInput, setTagInput] = useState('')
const handleChange = (e) => {
setTagInput(e.currentTarget.value)
}
const handleSubmit = (e) => {
e.preventDefault()
addTag(tagInput)
setTagInput("")
}
return (
<div className="search">
<input type="text" placeholder="Add a tag" value={tagInput} onClick={()=>{
setCancelTag(!cancelTag)
console.log("search enabled")
}} onChange={handleChange} />
{cancelTag? null : <button className="cancel-tag" onClick={handleSubmit} >×</button>}
</div>
}
Try the following. This would destructure addTag prop then assign/spread the rest of the properties to variable of name user:
const Profile = ({ addTag, ...user }) => {
Keep in mind, all props are passed as the first argument to the Profile function as an object with properties matching the name of the props passed to Profile.
When a new drum is added to the array of drums in setDrums each drum component is displayed, the user can give the drum a name, how do I add the name to the drum in the array drums?
I can logout the drum id, but how do I find that drum in the array and only update that drum with the name the user entered?
https://codesandbox.io/s/amazing-dan-dsudq?file=/src/index.js:0-1371
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
const Drum = ({ id, count, remove, editName, name }) => {
const [sounds, setSounds] = useState(count);
useEffect(() => {
if (sounds > 0)
setTimeout(() => {
console.log("Playing sound");
setSounds(sounds - 1);
}, 1000);
}, [sounds]);
return (
<div>
<p>Drum #{id}</p>
<p>Drum Name {name}</p>
<p>Remaining sounds: {sounds}</p>
<label>
Drum Name <input type="text" onChange={editName} />
</label>
<br />
<button onClick={remove}>Delete drum #{id}</button>
</div>
);
};
const App = () => {
const [drums, setDrums] = useState([]);
const [nextId, setNextId] = useState(0);
return (
<div>
{drums.map(drum => (
<Drum
key={drum.id}
id={drum.id}
count={drum.count}
remove={() => setDrums(drums.filter(other => drum.id !== other.id))}
editName={() => console.log(drum.id)} // <== how to save the entered name to setDrums?
name={drum.name}
/>
))}
<button
onClick={() => {
setDrums([
...drums,
{ id: nextId, count: Math.floor(Math.random() * 100) }
]);
setNextId(nextId + 1);
}}
>
Add drum
</button>
</div>
);
};
ReactDOM.render(<App />, rootElement);
Updated
I have updated the codesnadbox also.Link
Theory - To update the value we need an identifier of the array, which is the index of each drum.
I created an editDrumName function which accepts two parameters one is the event and the other is the id. Then I cloned the drums in a tempDrums variable and updated the value with the id.
You cannot do this to the child component as because the value passed in the props.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
const Drum = ({ id, count, remove, editName, name, index }) => {
const [sounds, setSounds] = useState(count);
useEffect(() => {
if (sounds > 0)
setTimeout(() => {
console.log("Playing sound");
setSounds(sounds - 1);
}, 1000);
}, [sounds]);
return (
<div>
<p>Drum #{id}</p>
<p>Drum Name: {name}</p>
<p>Remaining sounds: {sounds}</p>
<label>
Drum Name <input type="text" onChange={e => editName(e, index)} />
</label>
<br />
<button onClick={remove}>Delete drum #{id}</button>
</div>
);
};
const App = () => {
const [drums, setDrums] = useState([]);
const [nextId, setNextId] = useState(0);
const editDrumName = (event, id) => {
let tempDrums = drums;
tempDrums[id].name = event.target.value;
setDrums([...tempDrums]);
};
return (
<div>
{drums.map((drum, index) => (
<Drum
key={drum.id}
id={drum.id}
index-{index}
count={drum.count}
remove={() => setDrums(drums.filter(other => drum.id !== other.id))}
editName={editDrumName} // <== how to save the entered name to setDrums?
name={drum.name}
/>
))}
<button
onClick={() => {
setDrums([
...drums,
{ id: nextId, count: Math.floor(Math.random() * 100) }
]);
setNextId(nextId + 1);
}}
>
Add drum
</button>
</div>
);
};
ReactDOM.render(<App />, rootElement);
What about changing the array to an object then call the setDrums like this:
editName={(e) => setDrums({...drums, drums[drum.id].name: e.target.value )}
Since you can't edit the original array directly because react state should be treat as immutable, you should make a new referance of the array on every change.
editName={event => {
// set the new value to drum
drum.name = event.target.value;
// update drums state by creating shallow copy of the old one
setDrums(drums.slice(0));
}}
ref: http://facebook.github.io/react/docs/component-api.html
The correct way is to map thru the array, find the drum and update it.
We shouldn't mutate state directly. With objects, when we copy it and update a property of copied object, original object is mutated.
working demo is here
Like this
...
const editName = (e, id) => {
setDrums(
drums.map(item => {
if (item.id === id) {
return { ...item, name: e.target.value };
}
return item;
})
);
};
...
{drums.map(drum => (
<Drum
key={drum.id}
id={drum.id}
count={drum.count}
remove={() => setDrums(drums.filter(other => drum.id !== other.id))}
editName={e => editName(e, drum.id)} // <== how to save the entered name to setDrums?
name={drum.name}
/>
))}
{drums.map(drum => (
<Drum
key={drum.id}
id={drum.id}
count={drum.count}
remove={() => setDrums(drums.filter(other => drum.id !== other.id))}
editName={e => editName(e, drum.id)} // <== how to save the entered name to setDrums?
name={drum.name}
/>
))}
...
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;
}));
}
I have a json file with data. from this file I want to ask questions and the type of question. the structure is to look like this: there is a category name + picture, and then a list of questions.
This is my component that renders the list of questions.
import React from 'react';
import { sortBy } from 'lodash';
import QuestionsListItem from './QuestionListItem';
const images = require.context('../../img', true);
const imagePath = (name) => images(name, true);
const QuestionsList = ({ questions }) => {
const questionListItem = questions && questions.questions ?
sortBy(questions.questions, ['type']).map((question) => (
<>
{
const sortType = (question) => {
if(question.type==='science') {
<img src={imagePath('./star.png')} alt="star" />
<p>{question.type}</p>
}
}
}
<QuestionsListItem
key={question.id}
type={question.type}
question={question}
/>
</>
)) : null;
return (
<div>
<ul>
{ sortType }
{ questionListItem }
</ul>
</div>
);
};
At the beginning, I want to sort the array with a type, then I want to map it, then I want to render the type, if type ===" since " show the appropriate text and picture. At this element I get the error Parsing error: Unexpected token and points toconst sortType = (question) => {.... I do not know how to do it in React to get the effect I want. Where to insert the code responsible for type and category image rendering?
thanks in advance :)
You defined the function sortType in the JSX part of your file. This isn't allowed.
Moreover, in your sortType function, you must return JSX, and when you call your function, you must give it the question as argument.
Here would be the correct code:
import React from 'react';
import { sortBy } from 'lodash';
import QuestionsListItem from './QuestionListItem';
const images = require.context('../../img', true);
const imagePath = (name) => images(name, true);
const QuestionsList = ({ questions }) => {
const sortType = (question) => {
if(question.type==='science') {
return (<>
<img src={imagePath('./star.png')} alt="star" />
<p>{question.type}</p>
</>)
}
}
const questionListItem = questions && questions.questions ?
sortBy(questions.questions, ['type']).map((question) => (
<QuestionsListItem
key={question.id}
type={question.type}
question={question}
/>
)) : null;
return (
<div>
<ul>
{ sortType(question) }
{ questionListItem }
</ul>
</div>
);
};