A Truly Reactive Sortable Component
24 April 2014
You may recently have heard a lot of chanting on how data changing over time is the root of all evil, "the source of truth" and how React is all about "one directional data flow". These are all pretty words but what do they mean in practice?
To better illustrate this I'm going to take the Sortable React component that I built, and fix a major flaw in it's design.
Getting rid of manual DOM labour
The "drop" action is already being done reactively. This is nice because the change will reflect on any other component in UI that relies on the same data. But I'm still manually inserting a placeholder on drag. What if I wanted to reflect this state somewhere else in the application or vice versa?
Redefining the state
We need a minor update to the data structure in order for it to carry some more information about the state.
var data = {colors: ["Gold","Crimson","Hotpink","Blueviolet","Cornflowerblue","Skyblue","Lightblue"]};
Continuously re-rendering everything
Keeping track of DOM changes over time is hard, re-rendering everything based on a state is easier. Instead of manually removing and inserting a DOM placeholder node, we're going to keep track of what's being dragged by saving it in the state.
The code is pretty straightforward. The interesting part is the sort method, where we are setting a "dragging" property to the state
sort: function(colors, dragging) {var data = this.state.data;data.colors = colors;data.dragging = dragging;this.setState({data: data});},dragEnd: function() {this.sort(this.state.data.colors, undefined);},dragStart: function(e) {this.dragged = Number(e.currentTarget.dataset.id);e.dataTransfer.effectAllowed = 'move';// Firefox requires calling dataTransfer.setData// for the drag to properly worke.dataTransfer.setData("text/html", null);},dragOver: function(e) {e.preventDefault();var over = e.currentTargetvar dragging = this.state.data.dragging;var from = isFinite(dragging) ? dragging : this.dragged;var to = Number(over.dataset.id);if((e.clientY - over.offsetTop) > (over.offsetHeight / 2)) to++;if(from < to) to--;// Move from 'a' to 'b'var items = this.state.data.colors;items.splice(to, 0, items.splice(from,1)[0]);this.sort(items, to);},render: function() {var listItems = this.state.data.colors.map(function(item, i) {var dragging = (i == this.state.data.dragging) ? "dragging" : "";return (<li data-id={i}className={dragging}key={i}draggable="true"onDragEnd={this.dragEnd}onDragOver={this.dragOver}onDragStart={this.dragStart}>{item}</li>);}, this);return {listItems}}
And that's it: The Root of all Evil and The Source of truth!