From Booleans to Strings in Frontend State
- 5 minutes read - 1000 wordsWhen it comes to controlling frontend presentation, developers often rely on booleans. However, this approach can be limiting. In this post, I’ll explore the drawbacks of using booleans and introduce a more versatile alternative: plain old strings with type safety.
While this example uses React, the principles can be applied to other frontend frameworks as well.
The Setup
The React useState
hook gives us local state, 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 can then 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!
🚨 Additional feature request! Our stakeholder now wants three modals, one to create a 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);
This works, and it looks… okay!
Additional Additional Feature Request!
🚨 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.
// Meetup modals
const [createMeetupModalVisible, setCreateMeetupModalVisible] = useState(false);
const [updateMeetupModalVisible, setUpdateMeetupModalVisible] = useState(false);
const [cancelMeetupModalVisible, setCancelMeetupModalVisible] = useState(false);
// Organizer modals
const [createOrganizerModalVisible, setCreateOrganizerModalVisible] = useState(false);
const [updateOrganizerModalVisible, setUpdateOrganizerModalVisible] = useState(false);
const [cancelOrganizerModalVisible, setCancelOrganizerModalVisible] = useState(false);
// Topic modals
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.
What’s The Issue?
This issue here is the use of booleans.
Booleans are a blunt instrument. They have just two states, true
and false
(and
null
if you’re determined to confuse things). But many stateful interactions need
more than two states.
Consider our example above. What are all these state hooks trying to answer? It’s not:
“Is this modal visible, and this one, or this one, and this one…?”
Rather, it’s:
“Which modal is visible?”
Modals are singular; only one 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
. It’s 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.
The Solution: Strings in State
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 possible single 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.
Aren’t Strings Brittle?
But wait; isn’t relying on perfect strings brittle? One extra dash in
"new-meetup"
becomes "new--meetup"
, and no modal appears.
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--meetup"
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 arguably that isn’t any more
brittle than expecting booleans. You can even use a library like
Zod to fail at runtime if the array ever contains an
forbidden value.
Counteragument: YAGNI
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 pages 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.
Further Applications
I’ve used this technique to indicate which of many network requests may be in progress, precisely controlling a page full of state-dependent elements: 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);
}
Challenge
The next time you’re considering using a boolean to control state, ask yourself:
Is this, now and in the foreseeable future, a
true
/false
question?
Here a few common examples:
- Showing a form or a button. It’s not a boolean question; it’s a “form or a button” question.
- Page loading spinners. Modern SPA’s make a lot of network requests… why do we need to wait for everything to be complete? Not necessarily a boolean question.
The more you think about state management problems, the more you realize that very few are best handled by a boolean. Even those that are might not stay that simple forever.