Published: August 04, 2023 • Updated: September 05, 2023 • 5 min read
When it comes to controlling frontend presentation in React, booleans are a common tool, but they often fall short.
In this post, I’ll describe the limitations in this technique, and offer an alternative: plain old strings with type safety.
The React state hook gives us statefulness, and it’s often instantiated with a boolean. Open any complex React codebase, and you are sure to see something like this:
import { useState } from 'react';
const [modalVisible, setModalVisible] = useState(false);
When modalVisible
is true, the modal is visible. We show or hide the modal
with its setter:
const showModal = () => {
setModalVisible(true)
// ...form stuff happens
setModalVisible(false);
}
return modalVisible ? <Modal /> : null;
Everything seems to be going fine so far!
🚨 Additional feature request! Our stakeholder now wants three modals, one to create the thing, one to update the thing, and one to cancel the thing.
What should we do? The conventional answer is to add more state.
const [createModalVisible, setCreateModalVisible] = useState(false);
const [updateModalVisible, setUpdateModalVisible] = useState(false);
const [cancelModalVisible, setCancelModalVisible] = useState(false);
Looks okay!
🚨 Hang on; we have another request! Now we’re creating, updating, and canceling three different kinds of things on our page. I think you can imagine where this is going, but to be obnoxious, here’s the code.
const [createMeetupModalVisible, setCreateMeetupModalVisible] = useState(false);
const [updateMeetupModalVisible, setUpdateMeetupModalVisible] = useState(false);
const [cancelMeetupModalVisible, setCancelMeetupModalVisible] = useState(false);
const [createOrganizerModalVisible, setCreateOrganizerModalVisible] = useState(false);
const [updateOrganizerModalVisible, setUpdateOrganizerModalVisible] = useState(false);
const [cancelOrganizerModalVisible, setCancelOrganizerModalVisible] = useState(false);
const [createTopicModalVisible, setCreateTopicModalVisible] = useState(false);
const [updateTopicModalVisible, setUpdateTopicModalVisible] = useState(false);
const [cancelTopicModalVisible, setCancelTopicModalVisible] = useState(false);
In some interfaces like admin portals, it doesn’t stop at three.
Something isn’t right here. If we follow our nose to the code smell that made this component 700+ lines long, it’s these state hooks.
Booleans are a blunt instrument: they have two states, true and false, and null, if you’re intent on confusing things. Many stateful interactions need more than two states. Consider our example above. What are all these state hooks all trying to answer? It is not: “is this modal visible, and this one, and this one, and this one…?” Rather, it’s:
”Which modal is visible?”
Only one modal should be visible at a time. And so, this isn’t a case of true or false, it’s a case of which. A boolean is the wrong tool for this problem.
In State Management: How to tell a bad boolean from a good boolean,
Matt Pocock upended how I think about booleans in state. Following his example,
it is easy to assign three boolean variables in state called
loading
, error
, and complete
, and then create a world where all three are
true. Again, we have a which problem: in which state is the network request?
It should only ever be one. Creating a world where more than one can be true,
or none can be true, is confusing.
Here’s an alternative:
const [modalVisible, setModalVisible] = useState();
To show a modal for creating a Meetup event, we set a string value in state.
const showNewMeetupModal = () => {
setModalVisible('new-meetup');
// ...form stuff happens
setModalVisible(undefined);
}
const newMeetupModalVisible = modalVisible === 'new-meetup';
return newMeetupModalVisible ? <NewMeetupModal /> : null;
With this implementation, modalVisible
can hold infinite modals that could be
shown, or no modal, with a string or undefined
, or an empty string if you
prefer. The return logic can be abstracted to function containing a tidy switch
statement.
But wait; isn’t relying on correctly formed strings brittle? One typo of
"new-meeting"
for "new-meeetup"
and a modal fails to appear. Enter type
safety. We can type the modal with a TypeScript union, limiting what is
allowed.
type Modals = 'new-meetup' | 'edit-meetup';
const [modalVisible, setModalVisible] = useState<Modal>();
Now, setting "new-meeting"
as the modalVisible
is a type error.
I’ve done this at the frontend-backend interface, too. Rather than can_cancel
and can_reschedule
booleans, the API returns an array of actions you can take
on a record, such as ['cancel', 'reschedule']
. Then the frontend
conditionally exposes interfaces that support those actions. With type safety
we can limit what those actions are, that that isn’t any more
brittle I’d argue than expecting booleans. You can even use a library like
Zod to fail at runtime if the array ever contains an
forbidden value.
Maybe you aren’t going to need this added complexity? For simple pages, a boolean may be enough. But for larger pages with multiple interactions, this solution is superior. I’ve found that in a page with significant functionality, this is usually one of those reverse-YAGNI features, i.e. “you are going to need it” and you’re better off just adding it from the start.
I’ve used this technique it to indicate which modal is visible, and which network request may be in progress, precisely controlling a page full of spinners, disabled buttons, flash messages, toasts,etc.
type Actions = 'meetup-create' | 'meetup-update';
const [networkAction, setNetworkAction] = useState<Actions>();
const handleNewMeetupSubmit = async (payload: Payload) => {
setNetworkAction('meetup-create');
const meetup = await createMeetupViaApi(payload);
setMeetup(meetup);
setNetworkAction(undefined);
}
What are your thoughts on booleans in frontend code? Let me know!
Join 100+ engineers who subscribe for advice, commentary, and technical deep-dives into the world of software.