ReactJS: State, Props and Reusable Components

So here’s a thing.

As I build more components in ReactJS, I’m starting to uncover some interesting use cases. One such case that came up recently involved reusing components — a component that can be used both on its own and as part of another component. Let me explain.

A new feature on ProjectNom involves connecting your user profile to a Twitter account. To make this a more seamless experience, I decided to build a simple React component that surfaces the current state of your Twitter account.

For example, if you haven’t added your Twitter account yet:

Connect Twitter Account Example

If you want to add your Twitter account, then we have to send you to the Twitter website to authenticate, so we display the following:

Redirect to Twitter Example

And finally, if you’ve already connected your Twitter account:

Twitter Connected Example

As you can see, this component revolves heavily around manipulating a button, so I knew that would be the primary element in the render.

But thinking more generally, the second state with the busy indicator was something that jumped out as a component that I could use elsewhere on the site. So, I decided to build a “BusyButton” component which could be used both as part of this Twitter component but also on its own whenever I needed a button to display a “busy” state.

So first, let’s make a BusyButton suitable for our Twitter component. As described in my earlier post, it’s best practice to store state in the outermost parent component. Child components just use their props. So, using that philosophy, we can build our BusyButton component like so:

module.exports = React.createClass({
	render: function() {
		var buttonStyle = "btn-" + this.props.style;
				
		if (this.props.busy)
		{
			var busyLabel = this.props.busyLabel
				? React.DOM.span({style: {paddingLeft: 10}}, this.props.busyLabel)
				: "";

			return React.DOM.button({className: "btn " + buttonStyle, style: {minWidth: 75}, disabled: "disabled"},
				React.DOM.i({className: "fa fa-circle-o-notch fa-spin"}),
				busyLabel
			);
		}
		else
		{
			var icon = this.props.icon
				? React.DOM.i({className: "fa fa-" + this.props.icon, style: {paddingRight: 10}})
				: "";

			var attributes = {className: "btn " + buttonStyle, type: this.props.type, id: this.props.id, onClick: this.props.onClick};
			
			if (this.props.disabled) {
				attributes.disabled = "disabled";
			}

			return React.DOM.button(attributes,
				icon,
				this.props.label
			);
		}
	}
});

As you can see, this component is driven entirely with props. If this is a child component, then that makes sense. If the parent gets rendered again, it will pass new props to the child, and the child’s behavior will change when it renders.

So far so good.

But now we come to our second acceptance criteria. We want this button to have the same behavior on its own: a button that can be marked as busy, and have the same disabled state and indicator icon.

With our current component, we have to set everything with props. That’s fine for the component’s initial render, but if we want to change the component’s behavior after the fact, we have a problem. Props are supposed to be immutable — an initial state and nothing more. (see: Props in getInitialState Is an Anti-Pattern)

Truth be told, we could ignore this advice and use something like componentWillReceiveProps to make the component behave like we expect. The issue I have with this approach is that it ignores the fact we are fundamentally talking about changes in the component’s state. The “active” and “busy” behaviors are two different states that the button can be in. And the button can freely move between those states as necessary.

This was the conundrum: I needed the component to run from props in order to keep state isolated in one spot, but I also needed the component to maintain state so that it could be easily manipulated when used on its own. I never found a satisfactory answer to this problem, so I welcome any suggestions or best practices for others who may have encountered this scenario.

In the meantime, I’ve done something of a compromise:

module.exports = React.createClass({
	getInitialState: function() {
		return {
			busy: this.props.initialBusy,
			disabled: this.props.initialDisabled
		};
	},

	busy: function() {
		this.setState({busy: true});
	},
	
	activate: function() {
		this.setState({busy: false});
	},
	
	disable: function() {
		this.setState({disabled: true});
	},
	
	enable: function() {
		this.setState({disabled: false});
	},

	render: function() {
		var buttonStyle = "btn-" + this.props.style;
				
		if (this.state.busy)
		{
			var busyLabel = this.props.busyLabel
				? React.DOM.span({style: {paddingLeft: 10}}, this.props.busyLabel)
				: "";

			return React.DOM.button({className: "btn " + buttonStyle, style: {minWidth: 75}, disabled: "disabled"},
				React.DOM.i({className: "fa fa-circle-o-notch fa-spin"}),
				busyLabel
			);
		}
		else
		{
			var icon = this.props.icon
				? React.DOM.i({className: "fa fa-" + this.props.icon, style: {paddingRight: 10}})
				: "";

			var attributes = {className: "btn " + buttonStyle, type: this.props.type, id: this.props.id, onClick: this.props.onClick};
			
			if (this.state.disabled) {
				attributes.disabled = "disabled";
			}

			return React.DOM.button(attributes,
				icon,
				this.props.label
			);
		}
	}
});

I ended up using state, since it conceptually made sense for the component. But rather than manipulate state directly, I’ve exposed some custom functions on the component that allow its user (whether it be a parent component or custom JavaScript) to control whether the button is in an active or busy state.

The disadvantage is that you have to use these functions. Passing down props from the parent no longer works, because this component now has state, and state takes precedence over props. Luckily, this is a basic component with only two primary states (active & busy), so it’s easy to manage.

But it’s clear that React doesn’t have good support for this scenario, and I’m not entirely happy with this solution. For a framework that is built entirely around the concept of reusable components, it isn’t very clear how reusability is supposed to work.