bevacqua/react-dragula

Conflicting display order update when combined with setState (Maybe bug)

luojxxx opened this issue · 3 comments

I have a dynamically ordered array of data entries, and how I want it to work is initially populate it with the dataOrder and after the order is changed by dragula to save the new order to dataOrder. This would trigger a re-render of the list with the correct order.

However, when the drop event is detected, the handleReorder runs setState(dataOrder), and I think what happens is dragula modifies the display, but then the setState modifies the underlying order resulting in a displayed list that's out of order with the underlying data.

This seems like a common use-case, so I'm wondering what's the best practice for dealing with this? I've already figured out one potential solution, but it's kinda hacky and not the react way of writing the code.

I've included the example code to demonstrate what I'm talking about.

*Note using flux, redux may potentially solve this issue since it triggers whole app re-render.

import React from 'react';
import Dragula from 'react-dragula';

export default class App extends React.Component {
  constructor(props) {
      super(props);
      this.state={
          dataOrder: [1,2,3,4,5],
      };
      this.start = 0;
      this.end = 0;
      this.getIndexInParent = this.getIndexInParent.bind(this);
      this.dragulaDecorator = this.dragulaDecorator.bind(this);
      this.displayEntries = this.displayEntries.bind(this);
      this.handleReorder = this.handleReorder.bind(this);
  }

  handleReorder(start, end) {
    console.log(start,end)

    var dataOrder = this.state.dataOrder;

    dataOrder.move = function (old_index, new_index) {
        if (new_index >= this.length) {
            var k = new_index - this.length;
            while ((k--) + 1) {
                this.push(undefined);
            }
        }
        this.splice(new_index, 0, this.splice(old_index, 1)[0]);
        return this; // for testing purposes
    };

    var newList = dataOrder.move(start,end)

    this.setState({ dataOrder: newList})
    console.log(this.state.dataOrder)
  }

  getIndexInParent(el) {
    return Array.from(el.parentNode.children).indexOf(el)
  }

  dragulaDecorator(componentBackingInstance) {
      if (componentBackingInstance) {
        var options = {
          moves: function (el, container, handle, sibling) {
              let pickedLocation = this.getIndexInParent(el);
              this.start = pickedLocation;
              return handle.classList.contains('handle');
          }.bind(this)
      };

        var drake = Dragula([componentBackingInstance], options);

        drake.on('drop', (el, target, source, sibling) => {
          let droppedLocation = this.getIndexInParent(el);
          this.end = droppedLocation;
          this.handleReorder(this.start, this.end);
        });
      }}

    displayEntries() {
      var dataOrder = this.state.dataOrder;
      var results = dataOrder.map( (val) => {
        return <div><span className='handle'>XXX</span> {val.toString().repeat(val)} </div>
      })
      return results;
    }

  render () {

    return (<div className='container' ref={this.dragulaDecorator}>
      {this.displayEntries()}
    </div>)
  }
}

*Note tested with redux and does not solve the problem.

Discovered real answer. You need to apply a unique key (Index doesn't work) to the list elements in order for it to update properly. It might be a good idea to update the Github documentation to reflect this necessity. Functioning code below for people who are interested:

import React from 'react';
import Dragula from 'react-dragula';

export default class App extends React.Component {
  constructor(props) {
      super(props);
      this.state={
          dataOrder: [1,2,3,4,5],
          datakey: {1:154, 2:2476 , 3:37585, 4:486979, 5:59798807 }
      };
      this.start = 0;
      this.end = 0;
      this.getIndexInParent = this.getIndexInParent.bind(this);
      this.dragulaDecorator = this.dragulaDecorator.bind(this);
      this.displayEntries = this.displayEntries.bind(this);
      this.handleReorder = this.handleReorder.bind(this);
  }

  handleReorder(start, end) {
    console.log(start,end)

    var dataOrder = this.state.dataOrder;

    dataOrder.move = function (old_index, new_index) {
        if (new_index >= this.length) {
            var k = new_index - this.length;
            while ((k--) + 1) {
                this.push(undefined);
            }
        }
        this.splice(new_index, 0, this.splice(old_index, 1)[0]);
        return this; // for testing purposes
    };

    var newList = dataOrder.move(start,end)

    this.setState({ dataOrder: newList})
    console.log(this.state.dataOrder)
  }

  getIndexInParent(el) {
    return Array.from(el.parentNode.children).indexOf(el)
  }

  dragulaDecorator(componentBackingInstance) {
      if (componentBackingInstance) {
        var options = {
          moves: function (el, container, handle, sibling) {
              let pickedLocation = this.getIndexInParent(el);
              this.start = pickedLocation;
              return handle.classList.contains('handle');
          }.bind(this)
      };

        var drake = Dragula([componentBackingInstance], options);

        drake.on('drop', (el, target, source, sibling) => {
          let droppedLocation = this.getIndexInParent(el);
          this.end = droppedLocation;
          this.handleReorder(this.start, this.end);
        });
      }}

    displayEntries() {
      var dataOrder = this.state.dataOrder;
      var results = dataOrder.map( (val) => {
        return <div key={this.state.datakey[val]}><span className='handle'>XXX</span> {val.toString().repeat(val)} </div>
      })
      return results;
    }

  render () {

    return (<div className='container' ref={this.dragulaDecorator}>
      {this.displayEntries()}
    </div>)
  }
}