1

I'm trying to render a list of inputs in react and bind the input values to an array. I'm also trying to make it so the list items are removable. However, when I remove an item from the array, the input items are not updated how I would expect. Instead of removing the input that was removed from the middle of the array, the last input is removed and the middle input remains.

var Inputs = React.createClass({
    getInitialState: function() {
    return {
      inputarr: ['']
    };
    },
  render: function() {
    var self = this;
    return <div>{ this.state.inputarr.map(function (value, i) {
        return <div key={i}><input onChange={function (e) {self.onChangeInput(i, e)}}/>
      { i < (self.state.inputarr.length - 1) && <button onClick={function () {self.onRemove(i)}}>x</button>}
      </div>;
    })  }</div>;
  },
  onChangeInput: function (i, e) {
    this.state.inputarr[i] = e.target.value;
    if (this.state.inputarr[this.state.inputarr.length - 1] !== '') {
      this.state.inputarr.push('');
    }
    this.setState({
      inputarr: this.state.inputarr.slice(0)
    });
  },
  onRemove: function (i) {
    this.state.inputarr.splice(i, 1);
    this.setState({
      inputarr: this.state.inputarr.slice(0)
    });
  }
});

ReactDOM.render(
  <Inputs/>,
  document.getElementById('container')
);

You can run this in this fiddle: https://jsfiddle.net/vvd7hex9/1/

What happens?

  1. add something to the first input, a second will appear. Type in 3 different inputs.
  2. remove the second input using the x button.

The last input is removed.

What I expected to happen

The middle input to be removed and only 2 inputs should contain the contents in the inputarr array.

Why does this happen? How can I fix it to remove the correct input?

justspamjustin
  • 1,730
  • 3
  • 19
  • 30

2 Answers2

0

Ahhhh, this is a classic javascript problem. It has to do with your map statement. You can read more about the specific details here, but what it boils down to is that when the click events actually fire, the value of i is equal to inputarr.length - 1. To fix this, you need some way of preserving the value of i during each loop. The easiest way to do this is to change the click event to this:

<button onClick={self.onRemove(i)}>x</button>

and change onRemove to this:

onRemove: function (i) {
    var self = this;
    return function(e) {
      self.state.inputarr.splice(i, 1);
      self.setState({
        inputarr: this.state.inputarr.slice(0)
      });
    }
  }

Some more info about closures can be found here if you're unfamiliar

Community
  • 1
  • 1
taylorc93
  • 3,676
  • 2
  • 20
  • 34
  • I understand the problem you are referencing. But if you test this out in the fiddle, you'll see that the behavior that you describe is not happening. The value for `i` is correct when I add `console.log(i)` to `onRemove`. – justspamjustin Dec 13 '16 at 18:57
0

I think it would be better to have separate Input component and App component. Then you can create increment and decrement methods and pass them down from App to your Input components. I have build a little pen to show how you can achieve it.
I used some useful methods from lodash so take a look how them work.

https://codepen.io/dagman/pen/oYaYyL

The code itself.

class App extends React.Component {
    constructor(props) {
        super(props);
        this.increment = this.increment.bind(this);
        this.decrement = this.decrement.bind(this);

        this.state = { 
            quantity: [0],
        };
    }

    increment(value) {
        const { quantity } = this.state;

        this.setState({ 
            quantity: quantity.concat(_.last(quantity) + 1),
        });
    }

    decrement(el) {
        const { quantity } = this.state;
        this.setState({ quantity: _.without(quantity, el) })
    }

    render() {
        const inputs = this.state.quantity.map(x => (
            <Input 
                increment={this.increment}
                decrement={this.decrement}
                key={x}
                toDelete={x}
            />
        ));

        return (
            <form>
                {inputs}
            </form>
        );
    }

}

class Input extends React.Component {
    constructor(props) {
        super(props);
        this.onChange = this.onChange.bind(this);
        this.onBtnClick = this.onBtnClick.bind(this);

        this.state = { 
            value: '',
            shouldIncrementQuantity: true,
        };
    }

    onChange(e) {
        const value = e.target.value;
        this.setState({ value });
            if(value.trim().length > 0 && this.state.shouldIncrementQuantity) {
            this.setState({ 
                shouldIncrementQuantity: false,
            }, () => this.props.increment());
        }
    }

    onBtnClick(e) {
        e.preventDefault();
        this.props.decrement(this.props.toDelete);
    }

    render() {
        return (
            <p className="input-field">
                <input
                    type="text" 
                    value={this.state.value}
                    onChange={this.onChange}
                />
                <button onClick={this.onBtnClick}>x</button>
            </p>
        );
    }
}

ReactDOM.render(
    <App />,
    document.getElementById('root')
);
devellopah
  • 471
  • 2
  • 15
  • This definitely appears to work how I was expecting. However, I still don't understand the core reason why this solution works, and why my example doesn't work. – justspamjustin Dec 13 '16 at 20:10
  • to be honest i can't even read it without headace mainly because you put all stuff into one component and you state looks like a mess. – devellopah Dec 13 '16 at 21:17
  • You need to separete state into two pieces(input state which responsible for input value that changes when you type and app state which responsible for adding and removing new inputs fields). This separetion is critical in order for you to build something more complex upon it. Even if you get you example work, you should not build your app this way. It's simply unmaintainable. – devellopah Dec 13 '16 at 21:26