Navigating Local State in React

William Hitchcock
Inside the Embassy
3 min readAug 10, 2018

--

Does my component need state? If you work with React, you’ve probably asked yourself this question more times than you care to remember (and for good reason). State is confusing, and many libraries (Redux, Flux, MobX) have tried to abstract the problem away. While this works great for complex state, it is exceedingly cumbersome if you’re just trying to control some UI. Still, local state on simple UI components poses its own set of unique problems, namely that variations in the behavior of the component’s state create an additional source of truth. Let’s explore the stateful and stateless approaches, using a simple Dropdown component, and see how it is possible to have have your cake and eat it too.

“Smart” approach:

The first approach that comes to mind is the stateful or “smart” approach. In this example the Dropdownknows how to control its own internal state (whether or not it’s open). This is convenient for the implementer because it works right away. However, if the implementer ever needs to control the state of the component, things tend to get messy.

class Dropdown extends React.Component {
constructor(props) {
super(props)
}

state = { isOpen: false }
toggle = () => this.setState({ isOpen: !this.state.isOpen }) render() {
return (
<div>
<button onClick={this.toggle}>{this.props.label}</button>
{ this.state.isOpen
? <div className='dialog'>I'm open</div>
: null
}
</div>
)
}
}

Check out a demo of this: https://codepen.io/will-hitchcock/pen/bjewpz

This approach works fine most of the time, unless you need to control the component’s internal state from its container. Let’s say another control needs to modify Dropdown’s internal state. You could try to expose a prop that can override the state, but it doesn’t really get at the core issue with this use case. There is no definitive no source of truth: the component’s internal state or the external state. Quite the pickle.

“Dumb” approach:

Another approach is to make the component stateless and expose an API for controlling it externally.

class Dropdown extends React.Component {
constructor(props) {
super(props)
}
static propTypes = {
label: PropTypes.string,
isOpen: PropTypes.bool,
toggle: PropTypes.func
}
render() {
return (
<div>
<button onClick={this.props.toggle}>
{this.props.label}
</button>
{ this.props.isOpen
? <div className='dialog'>I'm open</div>
: null
}
</div>
)
}
}

Demo: https://codepen.io/will-hitchcock/pen/QBEKYj

This approach seems almost immediately painful, as it will require the implementer to implement the default state every time. On the flip side, it provides flexibility to control the component externally without any real hassle. Exposing a simple API for controlling the component via props means that it can be implemented in any way.

Enter the higher order component

At this point you might be thinking, “It would be really convenient to have both of these approaches available”. This way you avoid the potential nightmare of needing to control a stateful component externally, while also leaving the default use case accessible. Luckily we can very easily extract the stateful logic using a higher order component. Bonus: any other components that use the same logic for their state can share the same code.

const withToggle = WrappedComponent =>
class extends React.Component {
constructor(props) {
super(props)
}
state = { isOpen: false } toggle = () => this.setState({ isOpen: !this.state.isOpen }) render = () => (
<WrappedComponent
isOpen={this.state.isOpen}
toggle={this.toggle}
{...this.props}
/>
)
}
class Dropdown extends React.Component {
constructor(props) {
super(props)
}
static propTypes = {
label: PropTypes.string,
isOpen: PropTypes.bool,
toggle: PropTypes.func
}
render() {
return (
<div>
<button onClick={this.props.toggle}>
{this.props.label}
</button>
{ this.props.isOpen
? <div className='dialog'>I'm open</div>
: null
}
</div>
)
}
}
const ToggleableDropdown = withToggle(Dropdown)

Demo: https://codepen.io/will-hitchcock/pen/gjrPEY

Huzzah! Now we have a smart component ToggleableDropdown that we use in most cases, but also Dropdown which we can use when we need to control our component externally. Bonus: any other components that share the same workflow (panels, modals, tooltips, and alerts, just to name a few) can all reuse the withToggle higher order component!

If you want to see what else our engineering team is cooking up, you can join us at Ambassador!

--

--