Muktadir Khan
Muktadir Khan

Reputation: 148

Change the state of parent component on click of a child component with a specific key in React JS

I am making a Todo App in React JS, i'm fairly new to React JS.

I have made two components named AddComponent and TodoItem component. The AddComponent has an input and button to add the todos and it will render TodoItem as a child component whenever a todo is added using the AddComponent.

I also have to remove the child component TodoItem when a button inside it is clicked according to it's key, so it would remove that item from the state of the parent component AddComponent.

The problem i'm facing is i can't change the state anywhere else than the render() function, because there is where i'm dynamically generating the TodoItem components using the state.

And changing the state inside the render() function would result in an infinite loop. Please help how to resolve this problem.

I'm using Bootstrap 4 using a CDN for styling.

AddComponent.js

import React from 'react'
import shortid from 'shortid'
import TodoItem from './TodoItem';

class AddComponent extends React.Component {
    constructor() {
        super();
        this.state = {
            todoText: '',
            todoList: []
        }
        this.handleChange = this.handleChange.bind(this);
        this.handleClick = this.handleClick.bind(this);
        this.handleTodoClick = this.handleTodoClick.bind(this);
    }
    handleChange(e) {
        e.persist();
        this.setState((prevState) => {
            return {
                todoText: e.target.value,
                todoList: prevState.todoList
            }
        })
    }
    handleClick() {
        this.setState((prevState) => {
            return {
                todoText: prevState.todoText,
                todoList: [{
                    text: prevState.todoText,
                    id: shortid.generate()
                }, ...prevState.todoList]
            }
        })
    }
    handleTodoClick(id) {
        const newState = this.state.todoList.filter(todoL => todoL.id === id);
        this.setState({ todoList: newState });
    }

    render() {
        const todos = this.state.todoList.map((todo) => {
            return (
                <TodoItem key={todo.id} value={todo.text} click={this.handleTodoClick(todo.id)}>{todo.text}</TodoItem>
            )
        })
        return (
            <div>
                <div className="row">
                    <div className="col-sm-9">
                        <div className="form-group">
                            <input
                                type="text"
                                className="form-control"
                                placeholder="Add a Todo"
                                onChange={this.handleChange}
                            />
                        </div>
                    </div>
                    <div className="col-sm-3">
                        <button
                            type="button"
                            className="btn btn-primary btn-block"
                            onClick={this.handleClick}
                        >Add
                        </button>
                    </div>
                </div>
                <div>
                    {todos}
                </div>
            </div>
        )
    }
}

export default AddComponent

TodoItem.js

import React from 'react'

class TodoItem extends React.Component {
    constructor(props) {
        super(props);

    }
    render() {
        return (
            <div className="alert alert-secondary alert-dismissible fade show">
                <button
                    type="button"
                    className="close"
                    onClick={this.props.click}
                    data-dismiss="alert">&times;</button>
                {this.props.value}
            </div>
        )
    }
}

export default TodoItem

Upvotes: 1

Views: 1309

Answers (3)

Kerem atam
Kerem atam

Reputation: 2777

Try : https://codesandbox.io/s/hopeful-moon-duqfe

There were two problems; 1st one is filter and the 2nd one is handler callback function that is set to click prop.

1st problem : Filter function should filter that does not match your id so you should use NOT equal :

  handleTodoClick(id) {
    const newState = this.state.todoList.filter(todoL => todoL.id !== id);
    this.setState({ todoList: newState });
  }

2nd problem : When you set click prop as a handler callback function, you should set reference of the function itself without calling it. Not like this : click={this.handleTodoClick(todo.id). This will call the function and set the click prop to result of it, which is undefined since it does not return to anything.

So i have changed your click props as click={this.handleTodoClick}

    const todos = this.state.todoList.map(todo => {
      return (
        <TodoItem key={todo.id} id={todo.id} value={todo.text} click={this.handleTodoClick}>
          {todo.text}
        </TodoItem>
      );
    });

On child component, onClick is similary set to function itself onClick={() => this.props.click(this.props.id)} NOT like onClick={this.props.click(this.props.id)}.

import React from "react";

class TodoItem extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div className="alert alert-secondary alert-dismissible fade show">
        <button
          type="button"
          className="close"
          onClick={() => this.props.click(this.props.id)}
          data-dismiss="alert"
        >
          &times;
        </button>
        {this.props.value}
      </div>
    );
  }
}

Upvotes: 1

Alexander Staroselsky
Alexander Staroselsky

Reputation: 38757

Try the following by updating the handle for click in AddComponent then creating a handler within TodoItem that calls props click with the respective todo item id value. I'd recommend to just pass the entire todo so that you can access both the unique id and value in TodoItem:

AddComponent:

    const todos = this.state.todoList.map((todo) => {
        return (
            <TodoItem key={todo.id} todo={todo} click={this.handleTodoClick}>{todo.text}</TodoItem>
        )
    })

TodoItem:

import React from 'react'

class TodoItem extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.props.click(this.props.todo.id);
  }

  render() {
    return (
      <div className="alert alert-secondary alert-dismissible fade show">
        <button
          type="button"
          className="close"
          onClick={this.handleClick}
          data-dismiss="alert">&times;</button>
        {this.props.todo.value}
      </div>
    )
  }
}

export default TodoItem;

Upvotes: 0

Pavel Petrovich
Pavel Petrovich

Reputation: 764

Fix:

<TodoItem key={todo.id} todoId={todo.id} value={todo.text} click={this.handleTodoClick(todo.id)}>{todo.text}</TodoItem>

We can't use key as id. The key property is used by React under the hood, and is not exposed to us.

import React from 'react'

class TodoItem extends React.Component {
    constructor(props) {
        super(props);

    }
    render() {
        return (
            <div className="alert alert-secondary alert-dismissible fade show">
                <button
                    type="button"
                    className="close"

                    // look here
                    onClick={e => {this.props.click(this.props.todoId)}}


                    data-dismiss="alert">&times;</button>
                {this.props.value}
            </div>
        )
    }
}

export default TodoItem

Upvotes: 0

Related Questions