2

While fixing a bug in some old code I came across a cumbersome implementation of an inputs list, as exemplified in the following minimal example.

When you fill both inputs, the re-order algorithm works well.
However when only one input is filled, it just copies the value into the second input.
I think it has something to do with the key property of the Array.map function - that are currently set with the indices (which is considered an anti-pattern) - however there are no IDs I can use to map the data in a more meaningful way.

I'm currently putting aside this being a bad implementation, as I want to understand what's going on with these keys (or alternatively to find out I'm mistaking and the bug is somewhere else).

class MyTest extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isAscending: true,
      placeholders: ['first', 'second'],
      values: [1, 2],
      customLabels: {},
    }
  }
  
  reverse() {
    const { values, placeholders, customLabels, isAscending } = this.state;
    
    const sortedData = values.map((value, index) => ({
      value,
      placeholder: placeholders[index],
      customLabel: customLabels[index],
    }))
    .sort((a, b) => (isAscending ? 1 : -1) * (b.value - a.value));

    const newPlaceholders = [];
    const newValues = [];
    const newCustomLabels = {};
    
    sortedData.forEach((dataObject, index) => {
      const { value, placeholder, customLabel } = dataObject;
    	
      newPlaceholders.push(placeholder);
      newValues.push(value);
      if (customLabel) newCustomLabels[index] = customLabel;
    });
    
    console.log({ newCustomLabels }); // Here I can verify that `customLabels` is sorted as expected
    
    this.setState({
      isAscending: !isAscending,
      placeholders: newPlaceholders,
      values: newValues,
      customLabels: newCustomLabels,
    });
  }
  
  onChange(e, index) {
    const { customLabels } = this.state;

    const newCustomLabels = Object.assign({}, customLabels);
    newCustomLabels[index] = e.target.value;

    this.setState({
      customLabels: newCustomLabels,
    })
  }
  
  render() {
    const { placeholders, customLabels, values } = this.state;
    
    return (
      <div>
        {
          placeholders.map((placeholder, index) => (
            <div key={index}>
              <input
                placeholder={placeholder}
                value={customLabels && customLabels[index]}
                onChange={(e) => this.onChange.bind(this)(e, index)}
              />
              {` with value: ${values[index]}`}
            </div>
          ))
        }
        <button onClick={this.reverse.bind(this)}>Reverse Order</button>
      </div>
    )
  }
}

ReactDOM.render(<MyTest />, document.querySelector("#app"));
<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>

3
  • 1
    Definitely have to put a unique key for each data set. reactjs.org/docs/lists-and-keys.html#keys Commented Jan 23, 2020 at 19:40
  • @gaditzkhori Can you please explain the unwanted behaviour that is the reason for this best-practice that's explained in the documentation? Commented Jan 24, 2020 at 5:50
  • Pretty well explained here: reactjs.org/docs/reconciliation.html#recursing-on-children. As well as Dan Abramov codepen examples including reordering Commented Jan 24, 2020 at 6:55

1 Answer 1

2

As said on the comments, you can create a unique key in each render, that works, but you are making a mistake about controlled/uncontrolled input, probably you are seeing on the logs a warning telling you that. That means that the input field value must never be undefined (controlled, must set an onChange event callback) or be the default behavior, you cannot mix them. This render function will fix your bug:

render() {
    const { placeholders, customLabels = {}, values } = this.state;

    return (
      <div>
        {placeholders.map((placeholder, index) => (
          <div key={index}>
            <input
              placeholder={placeholder}
              value={customLabels[index] || ''}
              onChange={e => this.onChange(e, index)}
            />
            {` with value: ${values[index]}`}
          </div>
        ))}
        <button onClick={this.reverse.bind(this)}>Reverse Order</button>
      </div>
    );
  }

Edit: sandbox link: https://codesandbox.io/embed/zen-payne-xpern?fontsize=14&hidenavigation=1&theme=dark

Sign up to request clarification or add additional context in comments.

4 Comments

Not sure I understand what you were saying about controlled/uncontrolled input, would love to hear more! And your sandbox doesn't work :(
Really sry about that... I forgot to save the file before sharing it... I updated the link. And about controlled/uncontrolled input, this link is really helpful goshakkk.name/controlled-vs-uncontrolled-inputs-react
But the snippet I posted with the render function is correct
That's interesting, I was completely sure it's something with the keys, because changing them caused different behaviors. Thanks a lot for the info!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.