Hello Everyone !
I'm new to ReactJS, and I'm trying to do some simple projects to get the basics.
I started a little project consisting at adding and deleting user of a list with React Hooks (manipulating the state).
I can properly add a new User to my Userlist and display it, but when it comes to delete a user, nothing happened.
I found the solution, but I can't explain it, that's why I'm asking for your help !
Here is my App.js file with the DeleteUser function that works
import style from './App.module.css';
import React, { useState } from 'react';
import UserList from './components/UserList';
import UserForm from './components/UserForm'
let USERS = [
{
id: 1,
name: 'John',
age: 27
},
{
id: 2,
name: 'Mark',
age: 24
}
]
const App = () => {
const [userss, SetUsers] = useState(USERS);
const AddNewUser = (user) => {
SetUsers((prevList) => {
let updatedList = [...prevList];
updatedList.unshift(user);
return updatedList;
});
};
const DeleteUser = user => {
SetUsers((prevList) => {
let updated = prevList.filter(el => el.id !== user.id);
return updated;
});
};
return (
<div className={style.root}>
<UserForm liftUpNewUser={AddNewUser} />
<UserList users={userss} liftUpUserToDelete={DeleteUser} />
</div>
);
}
export default App;
My Question is:
Why does the DeleteUser function writtten this way (below) doesn't work ? Knowing that it is the same logic as the AddNewUser function.
const DeleteUser = user => {
SetUsers((prevList) => {
let updated = [...prevList];
updated.filter(el => el.id !== user.id);
return updated;
});
};
Sorry in advance for my english!
Hope someone can help me =)
This line in your code...
updated.filter(el => el.id !== user.id);
... is a no-op, as value of updated array never gets changed. filter returns a new array instead, and this new array gets assigned to a variable in the first snippet.
The side effect of this is that React won't have to compare those arrays by value: their references will be different. It wouldn't have been the case if filter worked the way you expected it to work, making the changes in-place, similar to Array.splice.
Because Array.filter method does not modify the original array but returns a new one.
Related
How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};
I am having a bit of an issue when attempting to .map a React state variable into various JSX elements. I have noticed that the state variable I am storing the array within is not undefined, as "console.log-ing" the variable appears to hold the information I need it to. The error according to my React Development Page mentions the error starts at line 43 Here is my code for the main Dashboard.js I am utilizing the .map function within to create new list items.
import React, {useState} from 'react';
import {generateId} from '../../Utilities/generateId.js';
import {expirationTime} from '../../Utilities/expirationTime.js';
import {Form} from '../Form/Form.js';
import {Thought} from '../Thought/Thought.js';
export function Dashboard() {
// Sets up sample message when site first loads up; eventually disappears after 15s
// generateId adheres an id number starting at 0, to each thought to easily identify each thought
// expirationTime sets the automatic removal of the "thought" 15 seconds away from current time
const [thoughts, setThoughts] = useState([
{
id: generateId(),
text: "Add your thoughts, don't be shy!",
expires: expirationTime(),
},
{
id: generateId(),
text: 'They disappear after 15 seconds!',
expires: expirationTime(),
}
]);
//adds new thought object to array lists out all collective thoughts in thoughts array
const addThought = (thought) => {
setThoughts((prev) => [thought, ...prev]);
};
const removeThought = (thoughtID) => {
//reuses state and uses filter function to cycle through each object in the thoughts array,
// and remove the thought whose ID matches the one that was selected(clicked) on
setThoughts((prev) => {
prev.filter((thought) => thought.id !== thoughtID)
});
};
return (
<div>
<h1>Thought Machine</h1>
<h3>Welcome! This is an open space for any of your thoughts.</h3>
<h2>Thanks for visiting!</h2>
<Form addThought={addThought} />
<ul className='thoughts'>
{thoughts.map( thought =>
(<Thought key={thought.id} thought={thought} removeThought={removeThought} />)
// thought.text
)}
</ul>
</div>
)
};
Aside from the main code here, here is my code for Thought.js, where I pass in my props for the "thoughts" I am attempting to individually list out. I do not believe there is any issue with my code here, yet I am posting it just in case.
Thought.js:
import React, {useEffect} from 'react';
export function Thought(props) {
const {thought, removeThought} = props;
useEffect(() => {
const timeRemaining = thought.expirationTime - Date.now();
const timeout = setTimeout(() => {
removeThought(thought.id)
}, timeRemaining);
return () => {
clearTimeout(timeout);
}
}, [thought]);
const handleRemoveClick = () => {
removeThought(thought.id);
};
return (
<li className='Thought'>
<button className='remove-button' onClick={handleRemoveClick}>×</button>
<p className='text' >{thought.text}</p>
</li>
)
};
I also do not believe that there is any issue with my code for the Form, yet here is that following code just in case my judgment is incorrect.
Form.js:
import React, {useState} from 'react';
import {generateId} from '../../Utilities/generateId.js';
import {expirationTime} from '../../Utilities/expirationTime.js';
import './Form.css';
export function Form(props) {
const [text, setText] = useState('');
//this function is called everytime the text value in the entry box is changed... adjusts state value of text
const handleTextChange = (event) => {
setText(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
//sets object for new thought and fills in new message; new generated ID and new time 15 seconds ahead of current time
const thought = {
id: generateId(),
text: text,
expiresAt: expirationTime(),
};
// checks if text has any text within it, then submits the new 'thought' to props.addThought
if (text.length > 0) {
props.addThought(thought);
// sets text state back to blank after submitting new thought
setText('');
}
}
return (
<form onSubmit={handleSubmit} className='Form'>
<input value={text} onChange={handleTextChange} type='text' placeholder="Go ahead ... there's no CIA agents here :)" />
<input type='submit' value='add' />
</form>
);
};
Apologies for the convoluted-ness of the question and thanks ahead of time for any assistance!
May be you are getting an error undefined at line 43 at the time of updating because of this code
const removeThought = (thoughtID) => {
setThoughts((prev) => {
return prev.filter((thought) => thought.id !== thoughtID) <---- You have to return as well
});
};
Explanation
When you are setting state while updating, you are returning nothing, thus the state is settled to undefined and we can't map over undefined.
dear community, I am facing a wired issue, and I don't know how to summary my situation in the question title, so I wonder if the question title is accurate enough.
I was trying to convert a class component to a hook component.
The class version code like this
async componentDidMount() {
const { dispatch, itemId } = this.props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment', //here to fetch submissions in props
payload: {
id: itemId
}
});
}
const { submissions } = this.props;
this.setState({
studentSubmissions: submissions,
});
} catch (error) {
throw error.message;
}
}
render() {
const { studentSubmissions } = this.state;
return (
<Table dataSource={studentSubmissions} />
)
}
export default SubmissionsDetail;
and in hook, it look like this
const [studentSubmissions, setStudentSubmissions] = useState([]);
useEffect(() => {
async function fetchSubmissions() {
const { dispatch, itemId } = props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment',
payload: {
id: itemId
}
});
}
const { submissions } = props;
setStudentSubmissions(submissions)
} catch (error) {
throw error.message;
}
};
fetchSubmissions()
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
I omitted some code for better reading, like connect to redux store or others.
and the component is import in the parent file like this
import SubmissionsDetail from './SubmissionsDetail'
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId}/>
})}
it work perfect in class component, the expected result should return tables like this
However, when I change to use hook, the result return like this
or sometimes all data in tables become submissions3
I try to console.log(submissions) inside the try{...} block, when in class, the result is
which is correct, there have two assignments, the one have 4 submissions, another one have zero submission.
But the output in hook is different, the result is like this
either both have 4 submissions, either both have zero. That means one obj affect all other obj.
It seems like if useState change, it would influence other objs, that make me really confused. I think in the map method, each item is independent, right? If so, and how to explain why it work perfectly in class setState, but failed in hook useState?
I hope my question is clear enough, If you know how to describe my question in short, plz let me know, I would update the title, to help locate experts to answer.
Please don't hesitate to share your opinions, I really appreciate and need your help, many thanks!
Edit: You are probably going to want to rework the way you store the submission inside of the redux store if you really want to use the Hook Component. It seems like right now, submissions is just an array that gets overwritten whenever a new API call is made, and for some reason, the Class Component doesn't update (and it's suppose to update).
Sorry it's hard to make suggestions, your setup looks very different than the Redux environments I used. But here's how I would store the submissions:
// no submissions loaded
submissions: {}
// loading new submission into a state
state: {
...state,
sessions: {
...state.session,
[itemId]: data
}
}
// Setting the state inside the component
setStudentSubmissions(props.submissions[itemId])
And I think you will want to change
yield put({
type: 'getSubmissions',
payload: response.data.collections
});
to something like
yield put({
type: 'getSubmissions',
payload: {
data: response.data.collections,
itemId: id
});
If you want to try a "hack" you can maybe get a useMemo to avoid updating? But again, you're doing something React is not suppose to do and this might not work:
// remove the useEffect and useState, and import useMemo
const studentSubmissions = useMemo(async () => {
try {
if (itemId) {
await dispatch({
type: "assignment/fetchSubmissionsByAssignment", //here to fetch submissions in props
payload: {
id: itemId,
},
});
return this.props.submissions;
}
return this.props.submissions;
} catch (error) {
throw error.message;
}
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
There is no reason to use a local component state in either the class or the function component versions. All that the local state is doing is copying the value of this.props.submissions which came from Redux. There's a whole section in the React docs about why copying props to state is bad. To summarize, it's bad because you get stale, outdated values.
Ironically, those stale values were allowing it to "work" before by covering up problems in your reducer. Your reducer is resetting the value of state.submissions every time you change the itemId, but your components are holding on to an old value (which I suspect is actually the value for the previous component? componentDidMount will not reflect a change in props).
You want your components to select a current value from Redux based on their itemId, so your reducer needs to store the submissions for every itemId separately. #Michael Hoobler's answer is correct in how to do this.
There's no problem if you want to keep using redux-saga and keep using connect but I wanted to give you a complete code so I am doing it my way which is with redux-toolkit, thunks, and react-redux hooks. The component code becomes very simple.
Component:
import React, { useEffect } from "react";
import { fetchSubmissionsByAssignment } from "../store/slice";
import { useSelector, useDispatch } from "../store";
const SubmissionsDetail = ({ itemId }) => {
const dispatch = useDispatch();
const submissions = useSelector(
(state) => state.assignment.submissionsByItem[itemId]
);
useEffect(() => {
dispatch(fetchSubmissionsByAssignment(itemId));
}, [dispatch, itemId]);
return submissions === undefined ? (
<div>Loading</div>
) : (
<div>
<div>Assignment {itemId}</div>
<div>Submissions {submissions.length}</div>
</div>
);
};
export default SubmissionsDetail;
Actions / Reducer:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchSubmissionsByAssignment = createAsyncThunk(
"assignment/fetchSubmissionsByAssignment",
async (id) => {
const response = await getSubmissionsByAssignment(id);
// can you handle this in getSubmissionsByAssignment instead?
if (response.status !== 200) {
throw new Error("invalid response");
}
return {
itemId: id,
submissions: response.data.collections
};
}
);
const initialState = {
submissionsByItem: {}
};
export default createReducer(initialState, (builder) =>
builder.addCase(fetchSubmissionsByAssignment.fulfilled, (state, action) => {
const { itemId, submissions } = action.payload;
state.submissionsByItem[itemId] = submissions;
})
// could also respond to pending and rejected actions
);
if you have an object as state, and want to merge a key to the previous state - do it like this
const [myState, setMyState] = useState({key1: 'a', key2: 'b'});
setMyState(prev => {...prev, key2: 'c'});
the setter of the state hook accepts a callback that must return new state, and this callback recieves the previous state as a parameter.
Since you did not include large part of the codes, and I assume everything works in class component (including your actions and reducers). I'm just making a guess that it may be due to the omission of key.
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId} key={itemId} />
})}
OR it can be due to the other parts of our codes which were omitted.
I have a component called StudentPage which shows the details of a student depending on the current student logged in currentStudent . The currentStudent object doesn't hold all the attributes of the Student model, so I fetch all the students with an axios request and get the student which matches the curresntStudent by id, and get all the attributes I need from it.
Now the problem is when I try to access the value of a student property, I get the error "TypeError: Cannot read property 'name' of undefined".
So I console.log- students, currentStudent, and student and noticed that when I try to access the currentStudent.name (commented below), the console.log-students outputs the list of students from my db, also noticed that the output of console.log-student object matches that of currentStudent. However, when I try to access student.name (code below), the console.log outputs shows an empty array for the students list causing the student object to be undefined.
Can anyone please help explain why I am noticing this behaviour, perhaps some concept of useEffect that I don't yet understand.
const StudentPage=({currentStudent, students, fetchStudents})=>{
useEffect(()=>{
fetchStudents();
},[]) //I tried making currentStudent a dependency [currentStudent] but same behaviour ocurs.
console.log(students);
console.log(currentStudent)
const student= students.filter(pupil=>{return(pupil.id===currentStudent.id)})[0]
console.log(student)
// return (<div>name: {currentStudent.name}</div>) this works normally but currentUser doesn't have all attributes i need
return(
<div>Name: {student.name}</div>
)
const mapStateToProps= state=>{
return {
students: state.students
}
}
export default(mapStateToProps,{fetchStudents})(StudentPage);
Here you can find an example on how to do what you want:
import React from "react";
import { useState, useEffect } from "react";
export function StudentPage({ studentId, fetchStudents }) {
const [student, setStudent] = useState();
useEffect(() => {
(async () => {
const students = await fetchStudents();
const currentStudent = students.find((student) => {
console.log(student);
return student.id === studentId;
});
setStudent(currentStudent);
})();
}, [studentId, fetchStudents]);
return (
<>
Student ID: {studentId}, Student name: {student && student.name}
</>
);
}
useEffect will be triggered whenever the dependency (studentId in this case) changes, including on first render. You should store you value in state (using useState) and have the necessary checks in place to see whether it is null or not.
I'm new to react and only understand the basics. I got this project from someone to look at, but I'm scratching my head since morning with this problem:
Uncaught TypeError: this.state.persons.map is not a function.
Please, if you can try to try to go over it in easy but in under the hood way. Thank You!
import React, { useState, Component } from 'react';
import './App.css';
import Person from './Person/Person';
import person from './Person/Person';
import { render } from '#testing-library/react';
class App extends Component {
state =
{
persons:[
{id: '123', name:'Max', age: 28 },
{id: '124',name:'Mari', age: 26 },
{id: '125',name: 'laly', age: 20 }
],
showPersons: false
}
nameChangeHandler=( event,id ) =>
{ const personIndex = this.state.persons.findIndex(p=>{
return p.id === id;
});
const person = {...this.state.persons[personIndex]
};
person.name=event.target.value;
const persons=[ ...this.state.persons];
persons[personIndex]=person;
this.setState(
{
persons:person
}
)
}
togglePersonHandler = ()=>
{
const doesShow = this.state.showPersons;
this.setState ({showPersons: !doesShow});
}
deletePersonHandler= (personIndex)=> {
//const persons = this.state.persons;
const persons = [...this.state.persons]
persons.splice(personIndex,1);
this.setState({persons:persons});
}
render()
{
const style ={
backgroundColor:'yellow',
font:'inherit',
border:'1px solid blue',
padding:'8px',
cursor:'pointer'
};
let persons=null;
if (this.state.showPersons){
persons= (
<div>
{this.state.persons.map((person,index)=> {return <Person click={() => this.deletePersonHandler(index)}
name = {person.name}
age = {person.age}
key={person.id}
change ={(event) => this.nameChangeHandler(event,person.id)}
/>
})};
</div>)
};
return (
<div className="App">
<h1>Hi This is react App</h1>
<button style={style} onClick={this.togglePersonHandler}> Toggle Persons</button>
{persons}
</div>
);
}
}
export default App;
Error lies here :
nameChangeHandler=( event,id ) =>
{ const personIndex = this.state.persons.findIndex(p=>{
return p.id === id;
});
const person = {...this.state.persons[personIndex]
};
person.name=event.target.value;
const persons=[ ...this.state.persons];
persons[personIndex]=person;
this.setState(
{
persons:person ------------ > You are assigning a single object to a list in your state, so your map is giving an error, it must be {persons: persons}
}
)
}
Whenever you see ___ is not a function, try looking at whatever the function is referring to.
In this case you're using map, which is an Array function. So you need to verify if the array (the thing at the left of the dot) is actually an array.
The function call:
this.state.persons.map
The array you need to pay attention to is persons. So try to look for the place in the code where persons is not getting recognized an array.
You have a typo here, you are assigning and object to state instead of an array. in the funcion nameChangeHandler.
this.setState(
{
persons:person
}
)
With a simple console inside the render or just watching it inside the Components Menu in the browser you can notice it bro.
It probably happens here, where you replace the array of persons with a single person:
this.setState(
{
persons: person
}
)
You probably want to do this instead:
this.setState({persons});