Two-way Data Binding in ReactJS - Part II
April 26th, 2017
In this post we explain how to use lodash to access nested object properties on a components state object so they can be bound to JSX form elements.
What do Aurelia, VueJS, and Angular have in common? All three are modern frameworks that support two-way data binding. Of course, ReactJS famously omits this feature, and I believe the standard excuse runs along the lines of, “Flux is better.”
Except it isn’t.
Write enough change handlers, and you will eventually realize that what you are doing amounts to implementing your own version of two-way data binding. Why re-invent the wheel this way, when onChange and value — the two attributes we use to bind state to our forms — can be abstracted into a couple of simple methods? In today’s post we’ll explore how to do this.
Consider the following component:
class Reservation extends Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2,
};
this.getChangeHandler = this.getChangeHandler.bind(this);
}
getChangeHandler(key) {
return function(event) {
if (event.target.getAttribute('type') === 'checkbox') {
this.setState({ key: event.target.checked });
} else {
this.setState({ key: event.target.value });
}
};
}
render() {
const { isGoing, numberOfGuests } = this.state;
return (
<div>
<form>
<label>
<input type="checkbox" value="{isGoing}" />
Attending
</label>
<label>
Number of guests:
<input type="number" value="{numberOfGuests}" />
</label>
</form>
</div>
);
}
}
In order to create this component, we had to write a (relatively) generic change handler, then repeat the work of attaching said change handler to each element that could trigger state changes, along with a value (or checked) attribute, and an argument representing the “key” of the state property to be updated on change. Our goal in Part I is to write a function that will eliminate all that work. It should have the following method signature:
function model(key = '') {
// ...
return {
/* ... */
};
}
We can use the return value to take advantage of the fact that JSX allows us to specify dynamic attributes, like this:
<input
type="checkbox"
{...{
isGoing: this.state.isGoing,
onChange: this.getChangeHandler('isGoing'),
}}
/>
Each property on the object after the span […] represents an attribute that should be added to the JSX tag; so, the above is equivalent to:
<input
type="checkbox"
value={isGoing}
onChange={this.getChangeHandler('isGoing')}
/>
What we need is a method that returns:
{
value: this.state[key],
onChange: this.getChangeHandler(key)
}
… for any property on the state object. To make this method reusable across components, we will return it as a function from a second method that accepts a component as its context:
function bindModel(context) {
return function(key) {
return {
value: context.state[key],
checked: context.state[key],
onChange: context.getChangeHandler(key),
};
};
}
But why add an identical copy of getChangeHandler(), to every component? Why not simply include it in our model binding instead?
function bindModel(context) {
return function(key) {
return {
value: context.state[key],
checked: context.state[key],
onChange(event) {
const target = event.target;
const value =
target.type === 'checkbox' ? target.checked : target.value;
context.setState({ key: value });
},
};
};
}
Now we can get a reference to this method in our render() function:
render() {
const model = bindModel(this);
// ...
… and call it to return a value/checked attribute and change handler for every element we want “bound” to our state:
<input type="checkbox" {...model('isGoing')}
// ...
<input type="text" {...model('numberOfGuests')}
Finally, change events may have additional side-effects, such as hiding or displaying buttons. In order to support this, our binder should call an optional, additional change handler after updating the component’s state:
function bindModel(context) {
return function(key) {
return {
value: context.state[key],
checked: context.state[key],
onChange(event) {
const target = event.target;
const value =
target.type === 'checkbox' ? target.checked : target.value;
context.setState({ key: value });
if (typeof context.handleChange === 'function') {
context.handleChange(key, value);
}
},
};
};
}
And there you have it! A reusable method that can bind state properties to JSX elements. This is sufficient for simple state properties; however, what if state contains nested objects? For example, the method we used here will not work for a state object that looks like:
this.state = {
isGoing: true,
guest1: { name: 'Alice' },
guest2: { name: 'Bob' },
};
In Part II, we will investigate how to solve this problem with lodash.
The full source for the example used in this article is at https://github.com/abraham-serafino/react-data-binding/tree/Part-I.
In this post we explain how to use lodash to access nested object properties on a components state object so they can be bound to JSX form elements.
Pre-compiling your Angular code can significantly reduce bundle size and improve performance. In this article we will review an example app to see it in action.
EDIT: Headless Chrome is shipping in Chrome 59 so the need to use the full Canary path will eventually go away. You can check your Chrome version in the menu under Help > About Google Chrome. This walkthrough shows you how to get headless Chrome up…
Abraham is a full-stack web enthusiast with experience in a wide variety of frameworks and technologies. Over the course of his career, Abraham has found himself working at large and small companies, driving best practices and providing uniquely inventive software solutions. He jumps at any opportunity to apply his knowledge of Spring and AngularJS.