ekud093
ekud093

Reputation: 17

how to properly address a state change in ReactJS

good people.

i have a small program that adds table row elements, when "Add" is clicked. There is also an color change option when table cell is clicked. The problem is - when multiple elements are created, clicking on one of them, changes the color for all of them, though i have a onClick sitting only on the TD tags. How could this be fixed?

https://jsfiddle.net/mattighof/5nmcyL7b/

<table>
          {this.state.list.map((element, index) => {
            return (
              <tr>
                <td
                  className={this.state.textColor ? "trRed" : "trBlack"}onClick={this.handleClick}>
                  {element}
                  <div
                    onClick={e => this.removeElement(e, index)}
                    className="div"
                  />
                </td>
              </tr>
            );
          })}
        </table>

would highly appreciate your advice.

Upvotes: 0

Views: 171

Answers (2)

Yalin Gunayer
Yalin Gunayer

Reputation: 46

Since you're generating <td> elements in an anonymous function, using this inside it will refer to the parent closure, which, in this case, is the Table component. Therefore, the textColor property is local to the component itself, and not to the individual <td> elements.

You're already iterating through a list that you keep in the component state, so you can slightly change the element structure to allow you to keep arbitrary state data individually.

To do this, instead of adding a raw string to your list, add an object with the text and isSelected properties set to the desired values, and when rendering the <td> elements or changing colors, use the said properties. You can, of course, name these properties to your liking, and even add more properties to individually manage the states of your elements.

One other thing to note is that the current implementation of the handleClick function is unaware of the context that you're calling it from, so you'll also need to pass the index of the element when calling it, and update your state with a new list where the element at the specified index has its state updated.

Here's the revised functions as per my naming:

addElement() {
  this.setState({
    list: this.state.list.concat({
      text: "element",
      isSelected: false
    })
  });
}
handleClick(e, index) {
  if (!this.state.list[index]) {
    return;
  }

  // to avoid any side effects, we're taking the immutable data approach
  // and creating a new list with intended values, rather than updating the list directly
  const oldElement = this.state.list[index];
  const newElement = Object.assign({}, oldElement, {isSelected: !oldElement.isSelected});

  const newList = [].concat(this.state.list);
  newList.splice(index, 1, newElement);

  this.setState({
    list: newList
  });
}
...
  {this.state.list.map((element, index) => {
    return (
      <tr>
        <td
          className={element.isSelected ? "trRed" : "trBlack"}
          onClick={e => this.handleClick(e, index)}
        >
          {element.text}
          <div
            onClick={e => this.removeElement(e, index)}
            className="div"
          />
        </td>
      </tr>
    );
  })}
...

I've also forked your fiddle and updated it with the code blocks I mentioned above: https://jsfiddle.net/w76frtgx/

Upvotes: 1

Juan Marco
Juan Marco

Reputation: 3241

There are many ways to handle this. Instead of relying on state to change the class, simply toggle the class trRed on the clicked target element.

To achieve this, modify handleClick to this:

handleClick(e) {
  e.target.classList.toggle("trRed")
}

Edit the style rule trRed to this:

.trRed {
  color: red;
}

And finally remove the textColor: true from state since it will no longer be used.

class Table extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      list: []
    };
    
    this.handleClick = this.handleClick.bind(this);
    this.addElement = this.addElement.bind(this);
    this.removeElement = this.removeElement.bind(this);
  }
  
  handleClick(e) {
	  e.target.classList.toggle("trRed")
  }
  
  addElement() {
    this.setState({ list: this.state.list.concat("element") });
  }
  
  removeElement(e, index) {
    e.stopPropagation();
    this.setState({ list: this.state.list.filter((_, i) => index !== i) });
  }

  render() {
    return (
      <div className="container">
        <button onClick={this.addElement} type="button">
          Add
        </button>
        <table>
          {this.state.list.map((element, index) => {
            return (
              <tr>
                <td
                  onClick={this.handleClick}
                >
                  {element}
                  <div
                    onClick={e => this.removeElement(e, index)}
                    className="div"
                  />
                </td>
              </tr>
            );
          })}
        </table>
      </div>
    );
  }
}


ReactDOM.render(<Table />, document.getElementById("app"));
body {
  padding: 20px;
}

td {
  border: 1px solid black;
  height: 15px;
  padding: 5px;
}

tr {
  border: 1px solid black;
  position: relative;
}

table {
  margin-top: 10px;
  text-align: center;
  width: 70px;
  border: 1px solid black;
  background-color: beige;
  border-collapse: collapse;
}

.trRed {
  color: red;
}

.div {
  float: right;
  width: 6px;
  height: 6px;
  background-color: red;
  cursor: pointer;
}
<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="app"></div>

Upvotes: 0

Related Questions