Reputation: 31
I'm trying to implement a sortable list of sortable lists in React, using React DnD. Prior to implementing the drag and drop side of things, all was working well.
I have a container component, which renders this:
<DndProvider backend={Backend}>
{this.state.classifications.map((classification, index) =>
<Classification id={classification.id} classification={classification} reportTemplate={this} key={classification.id} index={index} />
)}
</DndProvider>
The Classification extends Component, constructed like this:
constructor(props) {
super(props);
this.state = {
isEditing: false,
classification: props.classification
};
}
... and renders this (condensed for brevity):
<div className="panel-body">
<DndProvider backend={Backend}>
{this.state.classification.labels.map((label, index) =>
<Label id={label.id} label={label} reportTemplate={this.props.reportTemplate} key={label.id} index={index} />
)}
</DndProvider>
</div>
In turn, the Label also extends component, constructed like this:
constructor(props) {
super(props);
this.state = {
isEditing: false,
label: props.label
};
}
... and renders like this (again condensed for brevity):
return (
<div className={"panel panel-default panel-label " + (isDragging ? "dragging " : "") + (isOver ? " over" : "")}>
<div className="panel-heading" role="tab" id={"label-" + this.state.label.id}>
<div className="panel-title">
<div className="row">
<div className="col-xs-6 label-details">
{this.state.isEditing
? <input type="text" className="form-control" value={this.state.label.value} onChange={e => this.props.reportTemplate.onLabelValueChange(e, this.state.label.classificationId, this.state.label.id, 'value')} />
: <p className="form-control-static">{this.state.label.value}</p>
}
<div className="subuser-container">...</div>
</div>
</div>
</div>
</div>
);
All of this works well - when the user makes a change from the Label child component, it gets updated in the root component and everything syncs up and refreshes.
However, when implementing React DnD, both the Classification and Label components have been wrapped in Drag and Drop decorators, to provide sorting. The sorting via drag and drop works perfectly. However: this has caused the updating of elements to stop working (i.e., when a change is made from the Label, the update is fed through to the root Component correctly, but it doesn't then refresh down the tree).
Both the classification and label dnd implementations are like this in the render method:
return connectDropTarget(connectDragSource(...));
... and this when exporting the component:
export default DropTarget('classification', classificationTarget, classificationDropCollect)(DragSource('classification', classificationSource, classificationDragCollect)(Classification));
Interestingly, when a label is edited, the refresh does then occur when the user drags and drops the component. So its like the drag and drop will trigger a component refresh, but not the other onChange
functions.
That was a long question, apologies. I'm almost certain someone else will have experienced this issue, so any pointers gratefully appreciated.
Upvotes: 0
Views: 2036
Reputation: 31
Ok so i've basically answered my own question, but many thanks to those who posted comments on here to help narrow it down.
The answer was that my state object is complex and deep, and the component/decorator implementation of React DnD seems to have an issue with that. My assumption is that there is some behaviour in the decorator's shouldComponentUpdate
that is blocking the components refresh when a deep property is update. React DnD's own documentation refers to the decorators as "legacy", which is fair enough.
I updated our components to use hooks instead of decorators, and it all sprang to life. So the answer is this, if your DnD implementation is deep and complex, use hooks.
Upvotes: 3
Reputation: 39280
Here is an example of your code without DnD that won't work either because the prop is copied to state in the constructor and on concurrent renders the prop is never used again, only the state.
In Child you can see that it will render but counter never changes, in CorrectChild the counter will change because it's just using props.counter.
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {//copied only once in constructor
counterFromParent: props.counter,
};
}
rendered=0;
render() {
this.rendered++;
return (
<div>
<h3>in broken Child rendered {this.rendered} times</h3>
<button onClick={this.props.up}>UP</button>
<pre>
{JSON.stringify(this.state, undefined, 2)}
</pre>
</div>
);
}
}
class CorrectChild extends React.Component {
render() {
//not copying props.count, just using it
return (
<div>
<h3>in correct Child</h3>
<button onClick={this.props.up}>UP</button>
<pre>
{JSON.stringify(this.props, undefined, 2)}
</pre>
</div>
);
}
}
function App() {
const [state, setState] = React.useState({ counter: 1 });
const up = React.useCallback(
() =>
setState((state) => ({
...state,
counter: state.counter + 1,
})),
[]
);
return (
<div>
<h3>state in App:</h3>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
<Child counter={state.counter} up={up} />
<CorrectChild counter={state.counter} up={up} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
If you still experience that without DnD your code "works" then please provide a Minimal, Reproducible Example of it "not working".
Upvotes: 0