Jake Worth

Jake Worth

From Booleans to Strings in Frontend State

Published: August 04, 2023 • Updated: September 05, 2023 5 min read

  • react
  • booleans

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 Setup

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

🚨 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!

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.

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.

What’s The Issue?

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.

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 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.

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 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.

Further Applications

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.