react.js deleting one of few rendered components - javascript

Here is my first post.
I am in processs of creating todo app.
My app is adding new task to task list when plus button is clicked , and after few clicks u got few task simple , but the problem is that each task got delete Icon , which unfortunately by my lack of sufficient skills is deleting all components instead of the one , which icon belongs to.
Here is App.js code
class App extends React.Component {
constructor(props){
super(props);
this.handleDelete = this.handleDelete.bind(this);
this.addTask = this.addTask.bind(this);
this.state = {taskNum: 0,
delete: false
}
}
addTask(){
this.setState({
taskNum: this.state.taskNum +1,
delete:false
});
}
handleDelete(e){
console.log(e.target);
this.setState({delete: true});
}
render() {
const tasks = [];
for (let i = 0; i < this.state.taskNum; i += 1) {
tasks.push(!this.state.delete && <Task key={i} number={i} deleteTask={this.handleDelete}/>);
};
return (
<div className="ui container content">
<h2 className="centerHeader header">TODO LIST</h2>
<h3 className="taskheader secondaryHeader">Tasks <Icon className="addNew plus" action={this.addTask}/></h3>
<div className="ui container five column grid taskList">
{tasks}
</div> ...
and here is Task.js
export class Task extends React.Component {
constructor(props){
super(props);
this.dataChanged = this.dataChanged.bind(this);
this.state ={message: 'TASK' + (this.props.number+1),
}
}
customValidateText(text) {
return (text.length > 0 && text.length < 31);
}
dataChanged(data) {
console.log(data.message);
this.setState({message: data.message});
}
render(){
const centerRow = classNames('textCenter', 'row');
return (<div className="ui column task">
<div className={centerRow}><InlineEdit
validate={this.customValidateText}
activeClassName="editing"
text={this.state.message}
paramName="message"
change={this.dataChanged}
style={{
background: 'inherit',
textAlign:'center',
maxWidth: '100%' ,
display: 'inline-block',
margin: 0,
padding: 0,
fontSize: '1em',
outline: 0,
border: 0
}}
/></div>
<div className={centerRow}><Icon className="browser outline huge center"/> </div>
<div className={centerRow}><Icon className="large maximize"/><Icon className="large save saveTask"/><Icon className="large trash outline" action={this.props.deleteTask} /></div>
</div>
);
I thought of trying to e.target and select the parentNode but i am not sure if thats the proper solution since using react , so could u help me find efficient solution for this problem so that when trash icon is clicked it will delete only parent component.

One of the main reasons why react is powerful is that it give you the ability to maintain your DOM based on you data without having to do any manipulation to the DOM by yourself
So for your example to be more React JS friendly you need to do some changes, that will also fix your problem
1- You need to keep an actual state of your data, so adding a todo item means an actual real data added to your program, not just a component representation.
2- Removing a task represented by a component means removing the data it self, so deleting one of the tasks means deleting its actual data, and then react will handle updating the DOM for you, so the idea of a flag to hide a component is not React JS way
3- The way react knows when to render and how to handle all your data is managed by the component state .. so you save your data in component state and you only need to care about updating the state, then react will know that it will need to render again but now with the new updated list of tasks
I did some changes in your code with commented lines explaining why
class App extends React.Component {
constructor(props){
super(props);
this.handleDelete = this.handleDelete.bind(this);
this.addTask = this.addTask.bind(this);
this.state = {
tasks: [ ], // this is the array that will hold the actual data and based on its content react will handle rendering the correct data
counter : 1 // this counter is incremented every task addition just to make sure we are adding different task name just for the example explanation
}
}
}
addTask(){
this.setState({
tasks: this.state.tasks.push( "TASK" + counter); //This how we add a new task as i said you are adding an actual task data to the state
counter : this.state.counter + 1 //increment counter to have different task name for the next addition
});
}
//i changed this function to accept the task name so when you click delete it will call this function with task name as parameter
//then we use this task name to actually remove it from the tasks list data in our state
handleDelete(taskToDelete){
this.setState({
//the filter function below will remove the task from the array in our state
//After this state update, react will render the component again but now with the tasks list after removing deleting this one
tasks : this.state.tasks.filter((task) => {
if(task != taskToDelete)
return word;
})
});
}
render() {
const tasks = [];
//Notice here we pass an actual data to every Task component so i am adding prop message to take the value from this.state.tasks[index]
for (let i=0 ; i< this.state.tasks.length ; i++)
{
tasks.push(<Task key={i} message={this.state.tasks[i]} deleteTask={this.handleDelete}/>);
}
return (
<div className="ui container content">
<h2 className="centerHeader header">TODO LIST</h2>
<h3 className="taskheader secondaryHeader">Tasks <Icon className="addNew plus" action={this.addTask}/></h3>
<div className="ui container five column grid taskList">
{tasks}
</div> ...
)
}
}
Task Component
export class Task extends React.Component {
constructor(props){
super(props);
this.dataChanged = this.dataChanged.bind(this);
this.removeCurrentTask = this.removeCurrentTask.bind(this);
//Here i am setting the state from the passed prop message
this.state ={
message: this.props.message
}
}
customValidateText(text) {
return (text.length > 0 && text.length < 31);
}
dataChanged(data) {
console.log(data.message);
this.setState({message: data.message});
}
removeCurrentTask (){
//calling the deleteTask function with the current task name.
this.props.deleteTask(this.state.message);
}
render(){
const centerRow = classNames('textCenter', 'row');
return (
<div className="ui column task">
<div className={centerRow}><InlineEdit
validate={this.customValidateText}
activeClassName="editing"
text={this.state.message}
paramName="message"
change={this.dataChanged}
style={{
background: 'inherit',
textAlign:'center',
maxWidth: '100%' ,
display: 'inline-block',
margin: 0,
padding: 0,
fontSize: '1em',
outline: 0,
border: 0
}}
/></div>
<div className={centerRow}><Icon className="browser outline huge center"/> </div>
//Notice the action handler here is changed to use removeCurrentTask defined in current component
//BTW i don't know how Icon is handling the action property ... but now you can simply use onClick and call removeCurrentTask
<div className={centerRow}><Icon className="large maximize"/><Icon className="large save saveTask"/><Icon className="large trash outline" action={this.removeCurrentTask} /></div>
</div>
);
}
}

Related

React - state doesn't update in class component even though the message printed on the screen changes

I have an App component which holds an input. Every time I type in the input, the value of the input updates and a Message component prints a different message, depending on how long the input is. At the same time, a third component called Character print to the screen every letter of the string, individually. The desired behavior is that when I click on one of the letters, it gets removed from the string, the new string is displayed on the screen and the input also gets updated with the new string.
I used some console.logs to debug and everything seems to be happening as expected, until the last step when I am trying to update the state, but for some reason, it doesn't get updated.
class App extends React.Component {
constructor(props) {
super(props);
this.state = { text: "" };
}
render() {
const handleUpdateText = event => {
this.setState({
text: event.target.value
});
};
const inputLength = this.state.text.length;
const toArray = this.state.text.split("");
const handleDeleteLetter = index => {
toArray.splice(index, 1);
console.log(toArray);
const updatedArray = toArray.join("");
console.log(updatedArray);
this.setState({ text: updatedArray });
console.log(this.state.text);
};
return (
<>
<input type="text" onChange={handleUpdateText} />
<Message inputLength={inputLength} />
{toArray.map((letter, index) => (
<Character
key={index}
theLetter={letter}
deleteLetter={() => handleDeleteLetter(index)}
/>
))}
</>
);
}
}
class Message extends React.Component {
render() {
const { inputLength } = this.props;
let codeToPrint = "The text is long enough!";
if (inputLength <= 5) {
codeToPrint = "The text is not long enough!";
}
return <p>{codeToPrint}</p>;
}
}
class Character extends React.Component {
render() {
const { theLetter, deleteLetter } = this.props;
return (
<div
style={{
display: "inline-block",
padding: "16px",
textAlign: "center",
margin: "16px",
backgroundColor: "tomato"
}}
onClick={deleteLetter}
>
{theLetter}
</div>
);
}
}
The complete code is here:
https://codesandbox.io/s/react-the-complete-guide-assignment-2-list-conditionals-e6ty6?file=/src/App.js:51-1007
I don't really understand what am I doing wrong and I have a feeling is somehow related to a life cycle method. Any answer could help. Thank you.
State is getting updated, you just need to pass value prop to the input so that input's value can be in sync with your state
<input type="text" value={this.state.text} onChange={handleUpdateText} />
And you're not seeing updated state just after setting it because setState is asynchronous. That's why the console statement just after the setState statement shows the previous value.
Also you should move functions out of your render method, because everytime your component re-renders, new functions would be created. You can declare them as class properties and pass their reference
handleUpdateText = event => {
this.setState({
text: event.target.value
});
};
render() {
.......
return (
<>
<input type="text" onChange={this.handleUpdateText} />

State Change Causing Sidebar Reload

I am building an online store. When you click the checkout button, a sidebar slides into view showing the list of items in your cart. Inside of the cart is its list of items. You can change the quantity by toggling the up/down arrows in a Form.Control element provided by Bootstrap-React.
The way my code works is that when you toggle the up/down arrows to add or decrease the product quantity the state changes in the parent regarding what's in your cart. This triggers the child cart sidebar to close then reopen. I do not want this to happen! The sidebar should remain open.
I've tried two things; one is to use event.preventDefault() to try and make it so the page isn't refreshed, but this hasn't worked.
The other thing is trying to use shouldComponentUpdate and checking for whether the item quantity was changed, then preventing the app from re-rendering. This is the code I was using:
shouldComponentUpdate(nextProps, nextState) {
if (
nextState.cart &&
nextState.cart.length > 0 &&
this.state.cart.length > 0
) {
console.log("Next state cart num= " + nextState.cart[0].num)
console.log("curr state cart num= " + this.state.cart[0].num)
if (nextState.cart[0].num != this.state.cart[0].num) {
return false;
}
}
return true;
}
The problem is that my previous and future props are the same! Hence I can't write any code preventing re-rendering on item quantity change.
Can anyone provide some advice?
If your component is re rendering but its props and state aren't changing at all then you could prevent this with either React memo if you're using a function or if you're using a class based component then extending React.PureComponent instead of React.Component.
Both ways will do a shallow prop and state comparison and decide whether it should re render or not based on the result of said comparison. If your next props and state are the same as before then a re render will not be triggered.
Here's a codepen example so you can decide which one to use.
class App extends React.Component {
state = {
count: 0
};
handleClick = event => {
event.preventDefault();
this.setState(prevState => ({ count: prevState.count + 1 }));
};
render() {
return (
<div>
<span>Click counter (triggers re render): {this.state.count}</span>
<button style={{ marginLeft: "10px" }} onClick={this.handleClick}>
Click me to re render!
</button>
<SingleRenderClassComponent />
<SingleRenderFunctionComponent />
<AlwaysReRenderedClassComponent />
<AlwaysReRenderedFunctionComponent />
</div>
);
}
}
class SingleRenderClassComponent extends React.PureComponent {
render() {
console.log("Rendered React.PureComponent");
return <div>I'm a pure component!</div>;
}
}
const SingleRenderFunctionComponent = React.memo(
function SingleRenderFunctionComponent() {
console.log("Rendered React.memo");
return <div>I'm a memoized function!</div>;
}
);
class AlwaysReRenderedClassComponent extends React.Component {
render() {
console.log("Rendered React.Component");
return <div>I'm a class!</div>;
}
}
function AlwaysReRenderedFunctionComponent() {
console.log("Rendered function component");
return <div>I'm a function!</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));

Hide Font Awesome icon component and display another based on state

I'm building a pomodoro timer with 4 sessions. When a 25-minute timer is up, 1 is added to a state variable sessionNumber.
I have four of these circle/unchecked checkboxes displayed when the pomodoro cycle starts:
<FontAwesomeIcon
icon={faCircle}
size="2x"
className="pomodoro-unchecked session-checkbox"
/>
Each time 1 is added to sessionNumber state, I would like to hide one and display a different icon component:
<FontAwesomeIcon
icon={faCheckCircle}
size="2x"
className="pomodoro-checked session-checkbox"
/>
The only ways I could think of doing this would take a whole lot of code -- for example if statements based on each session number, like with an if statement, if the session is 0, display 4 unchecked circles (with the code for all four components), and if the session is 1, display 1 checked circle and 3 unchecked circles, and so forth. The second way I considered would be to give each one a different class name and in the method that changes the session number, display and hide each one, based on which session number it is (that would be more complicated). Is there a simpler, more succinct method?
You could use an array in state. You could initialise the array with four "faCircle" and then use the array.fill() method on setState to fill up the icons with "faCheckCircle". Then you can map over the array to render the appropriate icon.
class SessionIcons extends React.Component {
constructor(props) {
super(props);
this.state = {
sessions: 0,
icons: ["faCircle", "faCircle", "faCircle", "faCircle"]
};
}
onAddSession = () => {
this.setState(state => ({
sessions: state.sessions + 1,
icons: state.icons.fill("faCheckCircle", 0, state.sessions + 1)
}));
};
render() {
return (
<div>
{this.state.icons.map((icon, index) => (
<FontAwesomeIcon
key={icon + index}
icon={icon === "faCircle" ? faCircle : faCheckCircle}
size="2x"
/>
))}
<button onClick={this.onAddSession}>Add session</button>
</div>
);
}
}
Demo: https://codesandbox.io/s/shy-tdd-qzs1n
class Timer extends React.Component{
state = { sessionNumber: 1}
checkTimer = () =>{
..your logic to check timer every 25 mins
this.setState({timerState:1})
}
render(){
Let font;
if( this.state.sessionNumber == 1)
{
font = <FontAwesomeIcon
icon={faCircle}
size="2x"
className="pomodoro-unchecked session-checkbox"
/>
}
else if(this.state.sessionNumber == 2)
{
font = FontAwesomeIcon
icon={faCheckCircle}
size="2x"
className="pomodoro-checked session-checkbox"
/>
return(
{font}
)
}
}

Parent State not updating when passed data from child component

I'm trying to create a note-taking application in React.
The application should add a new note when an "Add note" button is pressed with the value in the input box.
Unfortunately when I try to push the note to the list and update the parents state the changes aren't reflected on screen or in the react debugger.
The pushing of new note to list can be seen in the alert line but not anywhere else.
Here is the parent component containing the original notes state:
class NoteApplication extends React.Component {
constructor(props) {
super(props);
this.state = {
notes: Array(),
};
this.update = this.update.bind(this);
this.state.notes.push("Sample note");
}
update(notes) {
return () => {
this.setState({
notes: notes
});
}
}
render() {
return (
<div>
<h1>React Notes</h1>
<div class="InsertBarDiv">
<InsertBar
notes={this.state.notes}
update = {this.update}
/>
</div>
<div class="NotesDiv">
<Notes
notes={this.state.notes}
/>
</div>
</div>
)
}
}
And here is the child component
class InsertBar extends React.Component {
constructor(props) {
super(props);
this.state = {value:''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
const notes = this.props.notes.slice();
notes.push(this.state.value);
this.props.update(notes);
alert(notes);
event.preventDefault();
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input class="noteInsertBar" type="text" name="" onChange={this.handleChange}/>
<input class="insertBut" type="submit" value="Add Note"/>
</form>
</div>
)
}
}
class Notes extends React.Component {
renderNote(i) {
return (
<div>
{this.props.notes}
</div>
)
}
render() {
return (
<div>
<h2>Notes:</h2>
<div class="FullNote">
{this.renderNote(1)}
</div>
</div>
)
}
}
I would expect the note to be pushed to the copy of the notes list & the parents state to be set to the new copy of the notes list.
I would then expect this to be displayed onscreen.
It's likely due to the fact that you're returning a function from update, you should just call setState when update gets called:
update(notes) {
setState({ notes });
}
Side note: You should avoid Array.push when dealing with arrays in React. The way you're doing it is fine because you're calling slice to copy the array before you push, but if you use concat or the spread operator, you'll be less likely to unintentionally introduce bugs.
const notes = this.props.notes.concat(this.state.value);
or:
const notes = [...this.props.notes, this.state.value];
I got some help in the react discord thanks to the user #WasaWasaWassup so I'd like to share what fixed my issue.
Mutating the parent state in the constructor to add a sample note was causing issues.
The second issue was my update function returning a function yet being called as if it wasn't.
Removing the constructor mutating & altering my update function to just set the state without an embedded function fixed all my issues and the notes array updates and displays correctly.

Rendering a list of items in React with shared state

Original Question
I'm trying to render a list of items using React. The key is that the items share a common state, which can be controlled by each item.
For the sake of simplicity, let's say we have an array of strings. We have a List component that maps over the array, and generates the Item components. Each Item has a button that when clicked, it changes the state of all the items in the list (I've included a code snippet to convey what I'm trying to do).
I'm storing the state at the List component, and passing down its value to each Item child via props. The issue I'm encountering is that the button click (within Item) is not changing the UI state at all. I believe the issue has to do with the fact that items is not changing upon clicking the button (rightfully so), so React doesn't re-render the list (I would have expected some kind of UI update given the fact that the prop isEditing passed onto Item changes when the List state changes).
How can I have React handle this scenario?
Note: there seems to be a script error when clicking the Edit button in the code snippet, but I don't run into it when I run it locally. Instead, no errors are thrown, but nothing in the UI gets updated either. When I debug it, I can see that the state change in List is not propagated to its children.
Edited Question
Given the original question was not clear enough, I'm rephrasing it below.
Goal
I want to render a list of items in React. Each item should show a word, and an Edit button. The user should only be able edit one item at a time.
Acceptance Criteria
Upon loading, the user sees a list of words with an Edit button next to each.
When clicking Edit for item 1, only item 1 becomes editable and the Edit button becomes a Save button. The rest of the items on the list should no longer show their corresponding Edit button.
Upon clicking Save for item 0, the new value is shown for that item. All the Edit buttons (for the rest of the items) should become visible again.
Problem
On my original implementation, I was storing an edit state in the parent component (List), but this state wasn't properly being propagated to its Item children.
NOTE: My original implementation is lacking on the state management logic, which I found out later was the main culprit (see my response below). It also has a bind bug as noted by #Zhang below. I'm leaving it here for future reference, although it's not really a good example.
Here's my original implementation:
const items = ['foo', 'bar'];
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
isEditing: false
};
}
toggleIsEditing() {
this.setState((prevState) => {
return {
isEditing: !prevState.isEditing
}
});
}
render() {
return (
<ul>
{items.map((val) => (
<Item value={val}
toggleIsEditing={this.toggleIsEditing}
isEditing={this.state.isEditing}/>
))}
</ul>
);
}
}
class Item extends React.Component {
render() {
return (
<li>
<div>
<span>{this.props.value}</span>
{ !this.props.isEditing &&
(<button onClick={this.props.toggleIsEditing}>
Edit
</button>)
}
{ this.props.isEditing &&
(<div>
<span>...Editing</span>
<button onClick={this.props.toggleIsEditing}>
Stop
</button>
</div>)
}
</div>
</li>
);
}
}
ReactDOM.render(<List />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<body>
<div id="app" />
</body>
you didn't bind the parent scope when passing toggleIsEditing to child component
<Item value={val}
toggleIsEditing={this.toggleIsEditing.bind(this)}
isEditing={this.state.isEditing}/>
I figured out the solution when I rephrased my question, by rethinking through my implementation. I had a few issues with my original implementation:
The this in the non-lifecycle methods in the List class were not bound to the class scope (as noted by #ZhangBruce in his answer).
The state management logic in List was lacking other properties to be able to handle the use case.
Also, I believe adding state to the Item component itself was important to properly propagate the updates. Specifically, adding state.val was key (from what I understand). There may be other ways (possibly simpler), in which case I'd be curious to know, but in the meantime here's my solution:
const items = ['foo', 'bar'];
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
editingFieldIndex: -1
};
}
setEdit = (index = -1) => {
this.setState({
editingFieldIndex: index
});
}
render() {
return (
<ul>
{items.map((val, index) => (
<Item val={val}
index={index}
setEdit={this.setEdit}
editingFieldIndex={this.state.editingFieldIndex} />
))}
</ul>
);
}
}
class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
val: props.val
};
}
save = (evt) => {
this.setState({
val: evt.target.value
});
}
render() {
const { index, setEdit, editingFieldIndex } = this.props;
const { val } = this.state;
const shouldShowEditableValue = editingFieldIndex === index;
const shouldShowSaveAction = editingFieldIndex === index;
const shouldHideActions =
editingFieldIndex !== -1 && editingFieldIndex !== index;
const editableValue = (
<input value={val} onChange={(evt) => this.save(evt)}/>
)
const readOnlyValue = (
<span>{val}</span>
)
const editAction = (
<button onClick={() => setEdit(index)}>
Edit
</button>
)
const saveAction = (
<button onClick={() => setEdit()}>
Save
</button>
)
return (
<li>
<div>
{ console.log(`index=${index}`) }
{ console.log(`editingFieldIndex=${editingFieldIndex}`) }
{ console.log(`shouldHideActions=${shouldHideActions}`) }
{
shouldShowEditableValue
? editableValue
: readOnlyValue
}
{
!shouldHideActions
? shouldShowSaveAction
? saveAction
: editAction
: ""
}
</div>
</li>
);
}
}
ReactDOM.render(<List />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<body>
<div id="app" />
</body>

Categories

Resources