React .map and setState weird behavior - reactjs

The following is a simplified version of the part of the entire code.
The entire app is basically supposed to be a note taking app built on React, and currently I'm stuck on its respective note's editing function.
So the following script part basically is supported to do:
Render an array of <Card /> components based on the App this.state.notes array
By clicking the Edit note button it sets the App this.state.noteEditingId state
(so the React instance can know later which generated Card is currnetly being edited by the id)
By clicking the Save Edit button it tries to update the App this.state.notes array with the submitted edit text.
(see, I used a lot of filters to try to achieve this, since I don't have a good idea to how to more nicely achieve this. I believe there should be a nicer way)
But the result is not what I expect.
(While it supposed to achieve updating the expected Card component's notes array with the new note instance's new note "note" text,
it updates the notes array with the different notes's note instance's "note" text. I cannot explain this clearly, since this is an idk-what-is-wrong type of issue to me. )
const Card = (props) => {
const [noteEditing, setNoteEditing] = useState(false);
return (
<div {...props}>
<div>
<div>
<span>
<button onClick={() => {
noteEditing ? setNoteEditing(false) : setNoteEditing(true);
props.thisApp.setState({ noteEditingId: props.config.id })
}}>Edit note</button>
</span>
{noteEditing
?
<div>
<textarea className='__text' />
<button onClick={() => {
let note = document.querySelector('.__text').value
let current_note = props.thisApp.state.notes.filter(a => a.id == props.config.id)[0]
let notesAfterRemoved = props.thisApp.state.notes.filter(a => a.id !== props.config.id)
if (props.thisApp.state.noteEditingId == props.config.id)
{
props.thisApp.setState({
notes: [...notesAfterRemoved, { ...current_note, note: note }]
})
}
}}>
Save Edit
</button>
</div>
: ""
}
</div>
</div>
</div>
)
}
class App extends React.Component {
constructor() {
super();
this.state = {
notes: [
{
note: "note1.",
id: nanoid(),
},
{
note: "note2.",
id: nanoid(),
},
{
note: "note3.",
id: nanoid(),
},
]
};
}
render() {
return (
<div>
<h2>
Notes ({this.state.notes.length})
</h2>
<div className='__main_cards'>
<div>
{this.state.notes.map((a, i) => {
return <Card key={i} className="__card" thisApp={this} config={
{
note: a.note,
}
} />
})}
</div>
</div>
</div>
)
}
}
So what can I do fix to make the part work properly? Thanks.

You should pass the current note also and get rid of the filter in card component:
this.state.notes.map((note, i) => {
return (
<Card
key={i}
className="__card"
thisApp={this}
currentNote={note}
/>
);
})
And then remove this:
let current_note = props.thisApp.state.notes.filter(a => a.id == props.config.id)[0]
And then rather than finding the notes without the edited note you can create a new array with edited data like this:
const x = props.thisApp.state.notes.map((n) => {
if (n.id === props.currentNote.id) {
return {...n, note}
}
return n
})
And get rid off this line:
let notesAfterRemoved = props.thisApp.state.notes.filter(a => a.id !== props.config.id)
Also, change this
noteEditing ? setNoteEditing(false) : setNoteEditing(true);
To:
setNoteEditing(prev => !prev)
As it is much cleaner way to toggle the value.
And I believe, there is no need to check that if the noteEditingId is equal to current active note id.
So you can get rid off that also (Correct me if I am wrong!)
Here's the full code:
const Card = (props) => {
const [noteEditing, setNoteEditing] = useState(false);
const textareaRef = useRef();
return (
<div {...props}>
<div>
<div>
<span>
<button
onClick={() => {
setNoteEditing((prev) => !prev); // Cleaner way to toggle
}}
>
Edit note
</button>
</span>
{noteEditing && (
<div>
<textarea className="__text" ref={textareaRef} />
<button
onClick={() => {
let note = textareaRef.current.value;
const x = props.thisApp.state.notes.map(
(n) => {
if (n.id === props.currentNote.id) {
return { ...n, note };
}
return n;
}
);
props.thisApp.setState({
notes: x,
});
}}
>
Save Edit
</button>
</div>
)}
</div>
</div>
</div>
);
};
class App extends React.Component {
constructor() {
super();
this.state = {
notes: [
{
note: "note1.",
id: nanoid(),
},
{
note: "note2.",
id: nanoid(),
},
{
note: "note3.",
id: nanoid(),
},
],
};
}
render() {
return (
<div>
<h2>Notes ({this.state.notes.length})</h2>
<div className="__main_cards">
<div>
{this.state.notes.map((note, i) => {
return (
<Card
key={i}
className="__card"
thisApp={this}
currentNote={note}
/>
);
})}
</div>
</div>
</div>
);
}
}
}
);
props.thisApp.setState({
notes: x,
});
}
}}
>
Save Edit
</button>
</div>
)}
</div>
</div>
</div>
);
};
class App extends React.Component {
constructor() {
super();
this.state = {
notes: [
{
note: "note1.",
id: nanoid(),
},
{
note: "note2.",
id: nanoid(),
},
{
note: "note3.",
id: nanoid(),
},
],
};
}
render() {
return (
<div>
<h2>Notes ({this.state.notes.length})</h2>
<div className="__main_cards">
<div>
{this.state.notes.map((note, i) => {
return (
<Card
key={i}
className="__card"
thisApp={this}
currentNote={note}
/>
);
})}
</div>
</div>
</div>
);
}
}
I hope this is helpful for you!

Related

When I press the button I want to add many Employees, but it only leaves me one. React

Good morning, I have a question. When I press the + button, only one employee line is added and I would like it to be added as many times as I press
ReactJS component code:
class Home extends React.Component {
state = { showForm:false }
showForm = () => {
return(
<Employee />
)
}
render() {
return (
<div className='container-home'>
<div className='min-margin'>
<Employee />
{this.state.showForm ? this.showForm() : null}
<div className='container-append'>
<button onClick={() => this.setState({showForm: true})}>➕</button>
</div>
</div>
</div>
)
}
}
You just click to show and hide the input.
You need:
Add to state array: (inputs: ["Employee-0"])
state = {
showForm: false,
inputs: ["Employee-0"]
};
Add to functions
handleAddInput = e => {
e.preventDefault();
const inputState = this.state.inputs;
let inputs = inputState.concat([`Employee-${inputState.length}`]);
this.setState({
inputs
});
};
handleShowForm = e => {
e.preventDefault();
this.setState({
...this.state,
showForm: !this.state.showForm
})
}
Change the code in render
render() {
return (
<div className="App">
{this.state.showForm && <form>
{this.state.inputs.map((input, idx) => (
<Employee key={idx}/>
))}
</form>}
<button onClick={this.handleAddInput}>Add New Employee</button>
<button onClick={this.handleShowForm}>Show form</button>
</div>
);
}
Click on the buttons)
The difference options exist for doing it , but that's work you did just a flag for shown of a Component. So you are able to try followings this:
class Home extends React.Component {
state = {
employeesCount: 0,
employees: []
}
render() {
return (
<div className='container-home'>
<div className='min-margin'>
{employees.map((eNumber) => {
return <Employee key={eNumber}/>
}}
<div className='container-append'>
<button onClick={() => this.setState({
employeesCount: employeesCount + 1,
employees: [...this.state.employess , (employeesCount + 1)]
})}>➕</button>
</div>
</div>
</div>
)
}
}
Try this:
import React from "react";
const Employee = (props) => {
return(
<div>Hello I am employee number {props.number}</div>
)
}
class App extends React.Component {
constructor() {
super()
this.state = { employees: [] }
}
addEmployee() {
this.setState({
employees: [...this.state.employees, <Employee number={this.state.employees.length} />]
})
}
render() {
return (
<div>
<div className='container-append'>
<button onClick={() => this.addEmployee()}>➕</button>
</div>
{ this.state.employees.map(employee => employee) }
</div>
)
}
}
export default App;

Re-render component based on object updating

I have the following pattern
class List {
list: string[] = [];
showList() {
return this.list.map(element => <div>{element}</div>);
}
showOptions() {
return (
<div>
<div onClick={() => this.addToList('value1')}>Value #1</div>
<div onClick={() => this.addToList('value2')}>Value #2</div>
<div onClick={() => this.addToList('value3')}>Value #3</div>
<div onClick={() => this.addToList('value4')}>Value #4</div>
</div>
);
}
addToList(value: string) {
this.list.push(value);
}
}
class App extends Component {
myList: List;
constructor(props: any) {
super(props);
this.myList = new List();
}
render() {
<div>
Hey this is my app
{this.myList.showOptions()}
<div>{this.myList.showList()}</div>
</div>
}
}
It shows my options fine, and elements are added to the list when I click on it. However, the showList function is never called again from App, thus not showing any update.
How can I tell the main component to rerenders when List is updated ? I'm not sure my design pattern is good. My goal is to manage what my class displays inside itself, and just call the display functions from other components.
We should always use state to rerender react component.
Not sure what you want to accomplish exactly but hopefully this will give you a general idea what Jim means with using state:
const Option = React.memo(function Option({
value,
onClick,
}) {
return <div onClick={() => onClick(value)}>{value}</div>;
});
const Options = React.memo(function Options({
options,
onClick,
}) {
return (
<div>
{options.map(value => (
<Option
key={value}
value={value}
onClick={onClick}
/>
))}
</div>
);
});
class List extends React.PureComponent {
state = {
options: [1, 2, 3],
selected: [],
};
showList() {
return this.list.map(element => <div>{element}</div>);
}
add = (
value //arrow funcion to bind this
) =>
this.setState({
options: this.state.options.filter(o => o !== value),
selected: [...this.state.selected, value],
});
remove = (
value //arrow funcion to bind this
) =>
this.setState({
selected: this.state.selected.filter(
o => o !== value
),
options: [...this.state.options, value],
});
render() {
return (
<div>
<div>
<h4>options</h4>
<Options
options={this.state.options}
onClick={this.add}
/>
</div>
<div>
<h4>choosen options</h4>
<Options
options={this.state.selected}
onClick={this.remove}
/>
</div>
</div>
);
}
}
const App = () => <List />;
//render app
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Error in Component Output by Button Click

I have 2 buttons and information about div. When I click on one of the buttons, one component should appear in the div info. Where is the error in the withdrawal of the component div info?
import React, { Component } from 'react';
import Donald from '/.Donald';
import John from '/.John';
class Names extends Component {
state = {
array:[
{id:1,component:<Donald/>, name:"Me name Donald"},
{id:2,component:<John/>, name:"My name John"},
],
currComponentId: null
changeComponentName = (idComponent) => {
this.setState({currComponentId:idComponent});
};
render() {
return(
<table>
<tbody>
<tr className="content">
{
this.state.array.map(item=> item.id===this.element.id).component
}
</tr>
<button className="Button">
{
this.state.array.map( (element) => {
return (
<td key={element.id}
className={this.state.currComponentId === element.id ? 'one' : 'two'}
onClick={ () => this.changeComponentName(element.id)}
>{element.name}
</td>
)
})
}
</button>
</tbody>
</table>
)
}
}
export default Names;
You have several problems here, the first being that you are missing the closing curly bracket on your state. this.element.id is also undefined, I assume you are meaning this.state.currComponentId.
Your html is also fairly badly messed up, for example you are inserting multiple <td>s into the content of your button. I also don't see where this.changeComponentName() is defined, so I am assuming you mean this.showComponent()
The primary issue is probably in this.state.array.map(item=> item.id === this.element.id).component, as map() returns an array. An array.find() would be more appropriate, though you still need to check to see if there is a match.
I might re-write your component like this (I have swapped out the confusing html for basic divs, as I'm not sure what you are going for here)
class Names extends Component {
state = {
array: [
{ id: 1, component: <span>Donald</span>, name: "Me name Donald" },
{ id: 2, component: <span>John</span>, name: "My name John" },
],
currComponentId: null,
};
showComponent = (idComponent) => {
this.setState({ currComponentId: idComponent });
};
render() {
//Finding the selected element
const selectedElement = this.state.array.find(
(item) => item.id === this.state.currComponentId
);
return (
<div>
<div className="content">
{
//Check to see if there is a selected element before trying to get it's component
selectedElement ? selectedElement.component : "no selected."
}
</div>
{this.state.array.map((element) => {
return (
<button
className="Button"
key={element.id}
className={
this.state.currComponentId === element.id ? "one" : "two"
}
onClick={() => this.showComponent(element.id)}
>
{element.name}
</button>
);
})}
</div>
);
}
}
Errors:- (1) You are showing list inside tag, instead show as <ul><li><button/></li></ul>(2)You are not displaying content after comparison in map()This is a working solution of your question.
class Names extends React.Component {
state = {
array: [
{ id: 1, component: <Donald />, name: "Me name Donald" },
{ id: 2, component: <John />, name: "My name John" }
],
currComponentId: null
};
clickHandler = idComponent => {
this.setState({ currComponentId: idComponent });
};
render() {
return (
<div>
<ul>
{this.state.array.map(element => {
return (
<li key={element.id}>
<button
className="Button"
onClick={() => this.clickHandler(element.id)}
>
{element.name}
</button>
</li>
);
})}
</ul>
{this.state.array.map(data => {
if (this.state.currComponentId === data.id)
return <div>{data.component}</div>;
})}
</div>
);
}
}
const Donald = () => <div>This is Donald Component</div>;
const John = () => <div>This is John Component</div>;
ReactDOM.render(<Names />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id='root' />

Update list of displayed components on deletion in React

in the beginning on my path with React I'm creating simple to-do app where user can add/remove task which are basically separate components.
I create tasks using:
addTask(taskObj){
let tasksList = this.state.tasksList;
tasksList.push(taskObj);
this.setState({tasksList : tasksList});
}
I render list of components (tasks) using following method:
showTasks(){
return (
this.state.tasksList.map((item, index) => {
return <SingleTask
taskObj={item}
removeTask = {(id) => this.removeTask(id)}
key = {index}/>;
})
);
}
method to remove specific task takes unique ID of task as an argument and based on this ID I remove it from the tasks list:
removeTask(uID){
this.setState(prevState => ({
tasksList: prevState.tasksList.filter(el => el.id != uID )
}));
}
But the problem is, when I delete any item but the last one, it seems like the actual list of components is the same only different objects are passed to those components.
For example:
Lets imagine I have 2 created componentes, if I set state.Name = 'Foo' on the first one, and state.Name='Bar' on the second one. If I click on remove button on the first one, the object associated to this component is removed, the second one becomes first but it's state.Name is now 'Foo' instead of 'Bar'.
I think I'm missing something there with correct creation/removing/displaying components in react.
Edit:
Method used to remove clicked component:
removeCurrentTask(){
this.props.removeTask(this.props.taskObj.id);
}
SingleTask component:
class SingleTask extends Component{
constructor(props) {
super(props);
this.state={
showMenu : false,
afterInit : false,
id: Math.random()*100
}
this.toggleMenu = this.toggleMenu.bind(this);
}
toggleMenu(){
this.setState({showMenu : !this.state.showMenu, afterInit : true});
}
render(){
return(
<MDBRow>
<MDBCard className="singleTaskContainer">
<MDBCardTitle>
<div class="priorityBadge">
</div>
</MDBCardTitle>
<MDBCardBody className="singleTaskBody">
<div className="singleTaskMenuContainer">
<a href="#" onClick={this.toggleMenu}>
<i className="align-middle material-icons">menu</i>
</a>
<div className={classNames('singleTaskMenuButtonsContainer animated',
{'show fadeInRight' : this.state.showMenu},
{'hideElement' : !this.state.showMenu},
{'fadeOutLeft' : !this.state.showMenu && this.state.afterInit})}>
<a
title="Remove task"
onClick={this.props.removeTask.bind(null, this.props.taskObj.id)}
className={
classNames(
'float-right btn-floating btn-smallx waves-effect waves-light listMenuBtn lightRed'
)
}
>
<i className="align-middle material-icons">remove</i>
</a>
<a title="Edit title"
className={classNames('show float-right btn-floating btn-smallx waves-effect waves-light listMenuBtn lightBlue')}
>
<i className="align-middle material-icons">edit</i>
</a>
</div>
</div>
{this.props.taskObj.description}
<br/>
{this.state.id}
</MDBCardBody>
</MDBCard>
</MDBRow>
);
}
}
Below visual representation of error, image on the left is pre-deletion and on the right is post-deletion. While card with "22" was deleted the component itself wasn't deleted, only another object was passed to it.
Just to clarify, the solution was simpler than expected.
In
const showTasks = () => taskList.map((item, index) => (
<SingleTask
taskObj={item}
removeTask ={removeTask}
key = {item.id}
/>
)
)
I was passing map index as a key, when I changed it to {item.id} everything works as expected.
In short, in the statement tasksList.push(<SingleTask taskObj={taskObj} removeTask ={this.removeTask}/>);, removeTask = {this.removeTask} should become removeTask = {() => this.removeTask(taskObj.id)}.
However, I would reconsider the way the methods addTask and showTasks are written. While the way you have written isn't wrong, it is semantically unsound. Here's what I would do:
addTask(taskObj){
let tasksList = this.state.tasksList;
tasksList.push(taskObj);
this.setState({tasksList : tasksList});
}
showTasks(){
return (
this.state.tasksList.map((item, index) => {
return <SingleTask
taskObj={item}
removeTask ={() => this.removeTask(item.id)}/>;
})
);
}
const SingleTask = (task) => {
const { taskObj } = task;
return <div onClick={task.removeTask}>
{ taskObj.title }
</div>
}
// Example class component
class App extends React.Component {
state = {
tasksList: [
{ id: 1, title: "One" },
{ id: 2, title: "Two" },
{ id: 3, title: "Three" },
{ id: 4, title: "Four" }
]
}
addTask = (taskObj) => {
let tasksList = this.state.tasksList;
tasksList.push(taskObj);
this.setState({tasksList : tasksList});
}
showTasks = () => {
return (
this.state.tasksList.map((item, index) => {
return <SingleTask
key={index}
taskObj={item}
removeTask ={() => this.removeTask(item.id)}/>;
})
);
}
removeTask(id) {
this.setState(prevState => ({
tasksList: prevState.tasksList.filter(el => el.id != id )
}));
}
render() {
return (
<div className="App">
<div> {this.showTasks()} </div>
</div>
);
}
}
// Render it
ReactDOM.render(
<App />,
document.body
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Updating props in note taking app in React

I'm stuck on my note taking app. Basically the App component passes in data to the NoteEntry component through props. Yet I can't figure out how to edit the previous passed text through props within each NoteEntry instance when I click the "edit" button. The edit button is supposed to bring up text inputs to change the content by updating the text and then pressing the save button. Any tips on how to go about it?
class App extends Component {
constructor(props) {
super(props);
this.state = {
notes: [],
title: "",
details: ""
}
this.updateTitle = this.updateTitle.bind(this);
this.updateDetails = this.updateDetails.bind(this);
this.submitHandler = this.submitHandler.bind(this);
this.deleteHandler = this.deleteHandler.bind(this);
}
updateTitle(event) {
this.setState({ title: event.target.value });
}
updateDetails(event) {
this.setState({ details: event.target.value });
}
submitHandler(e) {
e.preventDefault();
if (!this.state.title.length || !this.state.details.length) {
return;
}
const newNote = {
newTitle: this.state.title,
newDetails: this.state.details
}
this.setState(prevState => ({
notes: prevState.notes.concat(newNote),
title: "",
details: ""
}))
}
deleteHandler(id) {
this.setState(prevState => ({
notes: prevState.notes.filter(el => el !== id)
}))
}
render() {
return (
<div className="container">
<h1 className="title">React Notes App</h1>
<NoteForm
titleValue={this.state.title}
detailsValue={this.state.details}
titleHandle={this.updateTitle}
detailsHandle={this.updateDetails}
onSubmit={this.submitHandler}
/>
<div className="entry-section">
{this.state.notes.map((note, i) => (
<NoteEntry
key={i}
title={note.newTitle}
details={note.newDetails}
deleteNote={this.deleteHandler.bind(this, note)}
/>
))}
</div>
</div>
);
}
}
const NoteForm = (props) => {
return (
<div>
<form className="form-section">
<input
className="title-input"
type="type"
placeholder="Title"
value={props.titleValue}
onChange={props.titleHandle}
/>
<br />
<textarea
className="details-input"
cols="20"
rows="3"
placeholder="Details"
value={props.detailsValue}
onChange={props.detailsHandle}
/>
<br />
<button
className="input-button"
onClick={props.onSubmit}
>Add Note</button>
</form>
</div>
)
}
class NoteEntry extends Component {
constructor(props) {
super(props);
this.state = {
display: false,
editTitle: this.props.title,
editDetails: this.props.details,
editing: false
}
this.displayToggle = this.displayToggle.bind(this);
this.edit = this.edit.bind(this);
this.save = this.save.bind(this);
}
displayToggle() {
this.setState(prevState => ({
display: !prevState.display
}))
}
edit() {
this.setState({
editing: true
})
}
save() {
let titleVal = this.refs.updateTitle.value;
let detailsVal = this.refs.updateDetails.value;
this.setState({
editTitle: titleVal,
editDetails: detailsVal,
editing: false
})
}
render() {
return (
<div className="entry">
<div className="entry-header" onClick={this.state.editing ? null : this.displayToggle}>
{this.state.editing ? (
<input ref="updateTitle" className="edit-title" type="text" />
) : (
<h2 className="entry-title">{this.props.title}</h2>
)}
<p className="timestamp">{this.displayTime}</p>
</div>
<hr />
<div className={"entry-content " + (!this.state.display ? "hide-details" : null)}>
{this.state.editing ? (
<textarea ref="updateDetails" className="edit-details" cols="10" rows="2"></textarea>
) : (
<p className="details">{this.props.details}</p>
)}
<div className="entry-buttons">
{this.state.editing ? (
<button className="save" onClick={this.save}>Save</button>
) : (
<button className="edit" onClick={this.edit}>Edit</button>
)
}
<button className="delete" onClick={this.props.deleteNote}>Delete</button>
</div>
</div>
</div>
)
}
}
You can do by pass data from child to parent component as mention it in comment.
In you case NoteEntry add onEditNote props. This props use for function by parent (App component) and use by onClick edit button.
<NoteEntry
...
onEditNote={this.handleClickEdit}
/>
then in class NoteEntry
<button className="edit" onClick={() => this.props.handleClickEdit(this.props.title, this.props.detail)}>Edit</button>
So, handleClickEdit handle by App component and set it to your state
handleClickEdit = (_title, _detail) => {
this.setState({title: _title, details: _detail});
}
Now, your NoteForm component able to edit.

Resources