Some time ago, Bootstrap became a really big thing. And the patterns we learnt back then continue to be part of our development process. But using CSS conventions à la Bootstrap for the API design of your React components can lead to bad decisions.
The most famous example of this is the “button problem”: You have a Primary, Secondary, and Danger button. How do you expose this design? If we’re using Bootstrap’s CSS, the solution looks like this:
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-danger">Danger</button>
Seems normal, but what’s the expected behaviour of this one:
<button class="btn btn-primary btn-secondary">Primary or Secondary?</button>
If you know the order and understand your cascades, you know that the secondary style “wins” in this example. But, the real answer is that there is no expected behavior and you should wrap the library within your code in such a way that prevents this from happening.
Buttons and Booleans
We learnt this years ago, but it seems we need to re-learn it in React where the same problem takes a different form. I call it “the Boolean trap.” Often, I see folks writing code like this:
<button primary>Primary</button>
<button secondary>Secondary</button>
<button danger>Danger</button>
(Note: <Button primary>
is shorthand for <Button primary={true}>
.)
So what’s the expected behavior of this one:
<button primary secondary>Primary or Secondary?</button>
In case of Bootstrap and exposed CSS classes, you could at least make a really good guess. However, here it’s impossible to know without looking at the implementation.
If you think about components as functions, you have a function that looks like this:
function Button(primary: boolean, secondary: boolean, danger: boolean);
In Chapter 3 of Robert C. Martin’s “The Clean Code,” there is this brilliant bit on Flag Arguments (emphasis mine):
Flag arguments are ugly. Passing a boolean into a function is a truly terrible practice. It immediately complicates the signature of the method, loudly proclaiming that this function does more than one thing. It does one thing if the flag is true and another if the flag is false!
That’s how strongly “Uncle Bob” feels about a single boolean argument. Imagine what he’d say about 3 or more.
The reality is that this means that your Button component would better have been called MasterButtonSwitch
since it has to juggle a lot depending on its arguments. In the case of these 3 booleans, there would be 8 distinct states to handle.
The Type-checking Problem
If you wanted to enforce that only a single boolean was allowed to be given to the Button
component, you’d have to do some complicated enforcement: writing your own lint rules or maybe enforcing type safety via Union Types with Flow or TypeScript.
And it’s hard to call that a “solution.” The term “workaround” seems to better fit this approach.
Enums Over Booleans
A better approach can be seen in the use of enums over booleans. This results in the following API pattern:
<button variant="primary">Primary</button>
<button variant="secondary">Secondary</button>
<button variant="danger">Danger</button>
This works a lot better, but it still implies that the Button
component is doing some extra work behind the scenes.
Using a String
also opens a big issue from the perspective of developer experience: which enum strings can I use?
Without any additional tooling, the developer will need to jump into the Button
component to see what the implementation allows, or refer to some documentation (which might not exist.)
The improvement is, luckily, easy to implement thanks to React PropTypes.
Button.propTypes = {
variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
};
Your IDE/Code Editor of choice should pickup on this and provide the proper signature and auto-complete options.
You can also enforce that this prop is required:
Button.propTypes = {
variant: PropTypes.oneOf(['primary', 'secondary', 'danger']).isRequired,
};
Or assign a default in case it’s not provided:
Button.defaultProps = {
variant: 'primary',
};
Not a fan of magical strings? Well, you could create an actual enum that’s part of the Button
class:
<button variant="{Button.type.PRIMARY}">Primary</button>
<button variant="{Button.type.SECONDARY}">Secondary</button>
<button variant="{Button.type.DANGER}">Danger</button>
Higher Encapsulation
But this doesn’t feel quite right. And while the current API is much better, I’d much prefer we eliminated any enum or boolean props entirely. My proposal would be:
<PrimaryButton>Primary</PrimaryButton>
<SecondaryButton>Secondary</SecondaryButton>
<DangerButton>Danger</DangerButton>
All of these will simply be wrappers around our variant-based approach. So, for example, PrimaryButton
would just be:
const PrimaryButton ({ children, ...props }) =>
<Button variant="primary" {...props}>
{children}
</Button>
You can extract and share your PropTypes
to avoid their duplication and enable proper tooling for all the other props you might have.
So, we finally eliminated the usage of a boolean/enum prop and our library user (developer) can use a well-name component such as PrimaryButton
. This keeps in line with two important aspects of clean code:
- It follows the Principe of Least Surprise.
- It does one thing, and one thing only.
Subject to Change
Another important benefit is that this method of “wrapping” enables us to easily change the underlying implementation. Maybe the initial Button
component will stop using the variant
prop. All you have to do is update the PrimaryButton
. Not every instance where PrimaryButton
was ever used.
Another good example is the DangerButton
. Imagine you wanted to make that button always turn into a Confirm
button to double-check the user’s intention. This would require a whole different implementation.
If you had used <Button variant="danger">
all around your codebase, you would now be stuck with adding all of the logic into the Button
component itself.
But this way you have an out. The implementation of DangerButton
is completely open to change and it doesn’t even have to rely on Button
if you so desire.
A Bad Case of the “God Component”
Another example where I’ve seen the “boolean trap” is when dealing with forms. Here’s one that we have in our codebase right now:
<SomeForm isCreateForm="{someBoolean}" />
This comes with the same set of problems as our Button
conundrum:
- The
SomeForm
component probably does more than it should. - Without good documentation or looking at the implementation, there is no way of know what this prop does.
There’s an alternative to this. We can have two different forms and then use the boolean value to choose which one to render.
return isCreateForm ? <CreateForm /> : <EditForm />;
The initial SomeForm
was maybe doing this already. So it might seem like we took a step backwards. But this leads to the question: why are we even choosing between the forms?
Taking a step back would probably reveal that you have some EntityPage
that allows for entity creation and update. And you’re doing some “smart” switching based on the URL parameters. In this scenario, your routes probably look like this:
<Route path="/entity/new" component="{EntityPage}" /> <Route path="/entity/:id" component="{EntityPage}" />
Then, in the EntityPage, you check if the URL is entity/new
to render the CreateForm
, or otherwise show the EditForm
.
There’s many variations on this, but they boil down to one point of failure:
- A single “smart” component that does a lot of switching based on parameters and booleans.
But what you needed all along was:
<Route path="/entity/new" component="{EntityCreatePage}" />
<Route path="/entity/:id" component="{EntityViewPage}" />
<Route path="/entity/:id/edit" component="{EntityEditPage}" />
No booleans or switches required! And you can reuse all your cool components in all three pages.
Booleans and Control Flow
I hate seeing booleans in my codebase, but one case where you just can’t get rid of them seems to be external control flow.
For example when you want to use a dynamic control flag to disable an input, add a loading indicator, or control a modal. Usually, it looks like this:
<input disabled="{someBoolean}" />
<button loading="{someBoolean}" />
<Modal isOpen="{someBoolean}" />
Sometimes these tend to pile up:
<form loading="{someBoolean}" disabled="{someOtherBoolean}" />
And there isn’t really much we can do here. Unless, that is, we’re ready to cede control and stop controlling components with this approach.
In the case of the Modal
, we would let this component manage its own state and handle its open/close status. But how do we perform external triggers? How do we get it to open when some button on the page is clicked?
One way would be to dispatch actions (or events) and provide a way for the Modal
to subscribe to them.
Where you previously might have had a button that does:
handleOpenModal = () => this.setState({ isModalOpen: true });
You would now have something like:
handleOpenModal = () => this.props.dispatch({ type: OPEN_MODAL });
And the Modal would have to subscribe to this in order to set its own internal state in response.
That’s a lot more work on the part of the Modal
component. But from the point of view of the library user it might actually be easier. You need to dispatch the action instead of setting the local state, but you don’t need to pass that state over to the Modal
.
Whether one is better than the other is a question of preference, but both are viable. I would only argue that while either is okay, mixing the two isn’t.
The Boolean Checklist
There are of course cases in which Booleans are necessary. But as you’ve seen in the article, we often cling to them even though there are better solutions available.
Our finalized example with the buttons causes some code duplication, which is why developers tend to stay away from it. But, it results in a clearer (and cleaner!) API and it prevents inappropriate sharing of code and concerns.
So, next time you’re adding a boolean to your API, ask yourself:
- Is this boolean used to indicate a mutually exclusive control
- Will I use this boolean as a control mechanism to decorate a component with additional information?
- Does this boolean enable additional actions or flows?
If any of those are a “yes”, it’s a time to review your API and find a way to decouple your code.