primetwig / react-nestable

Drag & drop hierarchical list made as a react component
ISC License
215 stars 96 forks source link

Changes are not saved if setState is present in the onChange function #35

Closed jlei523 closed 4 years ago

jlei523 commented 4 years ago
        <Nextable
          items={this.state.items}
          ref={el => (this.refNestable = el)}
          onChange={this.onDragChange}
        />
  onDragChange(items, item) {
    this.setState({
      anystate: anychange
    })
  }

Whenever this.setState is in the onChange function, drag and drop does nothing. The items do not move.

Any ideas?

primetwig commented 4 years ago

Nestable is a controlled component. Do you update items in your state?

jlei523 commented 4 years ago

Thanks for the reply.

I don't update items in my state. In fact, I don't even have items in my state at all. It's an object outside of the React class.

Note that running this.setState for any state, even states that have nothing to do with Nestable, will cause Nestable to not apply drag changes.

onDragChange(items, item) {
    this.setState({
      anyState: "any change"
    });
  }
      <Nestable
        collapsed
        items={items}
        renderItem={renderItem}
        handler={<span style={handlerStyles} />}
        ref={ref}
        onChange={(items, item) => this.onDragChange(items, item)}
      />
primetwig commented 4 years ago

You have <Nestable items={items}. How exactly do you store items? If you create it during every render, than updating the local state would create a new instance of items, which would be equal the initial shape of items. Example: <Nestable items={items.slice(0)} will restore the initial items on every render of a parent component. Try to check (on every render) if you accidentally update the link to your items. Make sure you pass the same instance for every render. Or update it in onChange.

jlei523 commented 4 years ago

You have <Nestable items={items}. How exactly do you store items? If you create it during every render, than updating the local state would create a new instance of items, which would be equal the initial shape of items. Example: <Nestable items={items.slice(0)} will restore the initial items on every render of a parent component.

Right now I'm just storing it in an object outside of the class. I have a function to convert that object into an object that works with Nestable.

import Nextable from "./nestable.jsx";
import { convertCostCentersIntoItems } from "./util";

let costCentersTest = {
  "1": "HEAD OFFICE",
  "2": "UK",
  "3": "INTERNATIONAL",
  "2-4": "UK > MANCHESTER",
  "2-5": "UK > LONDON",
  "2-5-7": "UK > LONDON > VAN-1",
  "2-5-7-9": "UK > LONDON > VAN-1 > DRIVER-1",
  "2-5-8": "UK > LONDON > VAN-2",
  "2-5-8-10": "UK > LONDON > VAN-2 > DRIVER-2",
  "2-6": "UK > BIRMINGHAM"
};

class CostCenters extends Component {
  constructor(props) {
    super(props);
    this.state = {
      needsSave: false,
      newCostCenterName: "",
      userChangedItems: []
    };

    this.onDragChange = this.onDragChange.bind(this);
  }

  async componentDidMount() {
    const response = await fetch(baseUrl + "/api/cost_centres_get");
    const data = await response.json();
    console.log(data);
    this.setState({
      items: convertCostCentersIntoItems(data)
    });
  }

  collapse = collapseCase => {
    if (this.refNestable) {
      switch (collapseCase) {
        case 0:
          this.refNestable.collapse("NONE");
          break;
        case 1:
          this.refNestable.collapse("ALL");
          break;
        case 2:
          this.refNestable.collapse([1]);
          break;
      }
    }
  };

  onDragChange(items, item) {}

  updateInputValue = event => {
    this.setState({
      newCostCenterName: event.target.value.toUpperCase()
    });
  };

  submitNewCostCenter = () => {
    let obj = {
      id: 10,
      text: this.state.newCostCenterName
    };

    this.setState({
      items: this.state.items.concat(obj)
    });
  };

  render() {
    return (
      <div>
        <Controls
          updateInputValue={this.updateInputValue}
          submit={this.submitNewCostCenter}
          cancel={this.hideNewCostCenterInput}
          collapse={this.collapse}
        />

        <Nextable
          items={convertCostCentersIntoItems(costCentersTest)}
          ref={el => (this.refNestable = el)}
          onChange={this.onDragChange}
        />
      </div>
    );
  }
}
import React from "react";
import Nestable from "react-nestable";
import EdiText from "react-editext";

const handlerStyles = {
  position: "absolute",
  top: 0,
  left: 0,
  width: "12px",
  height: "100%",
  background: "steelblue",
  cursor: "pointer"
};

const styles = {
  position: "relative",
  padding: "10px 15px",
  fontSize: "20px",
  border: "1px solid #f9fafa",
  background: "#f9fafa",
  cursor: "pointer",
  display: "flex"
};

const renderItem = ({ item, collapseIcon, handler }) => {
  return (
    <div style={styles}>
      {handler}
      {collapseIcon}
      <EdiText showButtonsOnHover value={item.text} onSave={() => {}} />
    </div>
  );
};

export default ({ items, ref, onChange }) => {
  return (
    <div>
      <Nestable
        collapsed
        items={items}
        renderItem={renderItem}
        handler={<span style={handlerStyles} />}
        ref={ref}
        onChange={(items, item) => onChange(items, item)}
      />
    </div>
  );
};
primetwig commented 4 years ago

convertCostCentersIntoItems(costCentersTest) creates you a new instance of the Array every time component gets rerendered, which resets the internal state of Nestable.

jlei523 commented 4 years ago

convertCostCentersIntoItems(costCentersTest) creates you a new instance of the Array every time component gets rerendered, which resets the internal state of Nestable.

What's a way to fix this? Thanks!

primetwig commented 4 years ago

Try to use memo pattern. convertCostCentersIntoItems should (as an example) make a "shallow equal" check and return a new instance only when data was actually changed.

jlei523 commented 4 years ago

Try to use memo pattern. convertCostCentersIntoItems should (as an example) make a "shallow equal" check and return a new instance only when data was actually changed.

If I use one of the example objects that doesn't need any formatting, it's still the same issue. IE. If I clone the example in the repo, add a this.setState to the onChange function, it'll still have the same issue.

Is there a pattern that you recommend without having to resort to using memo?

primetwig commented 4 years ago

Check this part: https://github.com/primetwig/react-nestable/blob/master/src/Nestable/Nestable.js#L85 You have at least these 2 to be always new on every render:

handler={<span style={handlerStyles} />}
onChange={(items, item) => onChange(items, item)}

handler can be saved into top level variable. onChange can be passed as is.

jlei523 commented 4 years ago

Check this part: https://github.com/primetwig/react-nestable/blob/master/src/Nestable/Nestable.js#L85 You have at least these 2 to be always new on every render:

handler={<span style={handlerStyles} />}
onChange={(items, item) => onChange(items, item)}

handler can be saved into top level variable. onChange can be passed as is.

Ahh! The issue seemed to be handler. If I remove handler, it worked without changing anything else.

      <Nestable
        collapsed
        items={items}
        renderItem={renderItem}
        ref={ref}
        onChange={onChange}
      />

handler can be saved into top level variable. Can you show me what you mean by this?

This caused the same rerender issue.

  const Handler = () => {
    return <span style={handlerStyles} />;
  };

      <Nestable
        collapsed
        items={items}
        renderItem={renderItem}
        handler={<Handler />}
        ref={ref}
        onChange={onChange}
      />
primetwig commented 4 years ago
const handler = <span style={handlerStyles} />;
<Nestable handler={handler} />
jlei523 commented 4 years ago
const handler = <span style={handlerStyles} />;
<Nestable handler={handler} />

Worked! Thank you for your help!