Do not use boolean flags for your react state

marcinzmudka

Marcin Żmudka

Posted on February 23, 2024

Do not use boolean flags for your react state

Recently, I found myself tasked with refactoring a component designed for displaying posts in a client's web app, I encountered significant complexity. Initially designed as MVP, our app started with just two user actions on a post: delete and edit. However, as the project evolved, we expanded its functionality to include actions like report, hide, and share. Unfortunately, our original approach to manage these actions and trigger the corresponding modals (whether it was a confirmation for deletion or an edit form pre-filled with the post's data) fell short of scalability. We found ourselves entangled in a web of useState hooks, leading to a cluttered and hard-to-maintain component.

In this article, I aim to share the lessons learned from this experience. I'll detail the pitfalls of our initial design and demonstrate a more streamlined approach to managing component state, aiming to improve scalability and maintainability. Whether you're building or refactoring React applications, the insights provided here should help you avoid the complexities we faced and ensure your components are both efficient and easy to manage.

Initial Component Design

Our initial component closely mirrored an example from the Material-UI documentation, featuring a straightforward IconButton that triggers a menu with two items. Selecting an item from this menu would open the corresponding modal. The display logic for these modals relied on individual boolean useState hooks. Each action (be it editing or deleting) had its own useState to control the visibility of its modal, showcasing a direct but fragmented approach to state management.

export default function MoreOptions() {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);

  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
  const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false);

  const handleEdit = () => {
    setIsEditModalOpen(true);
    handleClose();
  };

  const handleDelete = () => {
    setIsDeleteModalOpen(true);
    handleClose();
  };

  return (
    <>
      <Box>
        <IconButton
          aria-label="more"
          id="long-button"
          aria-controls={open ? "long-menu" : undefined}
          aria-expanded={open ? "true" : undefined}
          aria-haspopup="true"
          onClick={handleClick}
        >
          <MoreVertIcon />
        </IconButton>
        <Menu
          id="long-menu"
          MenuListProps={{
            "aria-labelledby": "long-button",
          }}
          anchorEl={anchorEl}
          open={open}
          onClose={handleClose}
        >
          <MenuItem onClick={handleEdit}>Edit</MenuItem>
          <MenuItem onClick={handleDelete}>Delete</MenuItem>
        </Menu>
      </Box>
      <ConfirmationModal
        isOpen={isDeleteModalOpen}
        onClose={() => setIsDeleteModalOpen(false)}
      />
      <ConfirmationModal
        isOpen={isEditModalOpen}
        onClose={() => setIsEditModalOpen(false)}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Problem & The Solution

As our application evolved and we introduced new functionalities, the number of useState hooks within our component began to expand significantly. With each new feature (be it sharing, reporting, or hiding posts) came an additional boolean useState to manage its modal's visibility. This burgeoning complexity wasn't just a matter of unwieldy code; it posed a real risk to the application's reliability. Consider the scenario where, due to a coding error, multiple states are set to true simultaneously. This could result in overlapping modals, confusing the user and disrupting the user experience.


// Beginning of the component

const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false);
const [isShareModalOpen, setIsShareModalOpen] = React.useState(false);
const [isReportModalOpen, setIsReportModalOpen] = React.useState(false);
const [isHideModalOpen, setIsHideModalOpen] = React.useState(false);

// Rest of the component

Enter fullscreen mode Exit fullscreen mode

To address these issues, we needed a more scalable and error-resistant approach. The solution lay in consolidating our state management. Instead of maintaining separate states for each modal, we could use a single state variable to track the currently active modal, if any. This approach not only simplifies the state management by reducing the number of useState hooks but also inherently prevents the possibility of multiple modals being open at the same time. By adopting a more centralized state management strategy, we could enhance both the maintainability of our code and the integrity of the user experience.

To streamline our component's state management and address the issues arising from multiple boolean states, we introduced an enum to represent the different modal variants. This enum defines each action (Edit, Delete, Hide, Report, Share) as a distinct value, allowing us to replace the multiple boolean states with a single state variable that tracks the current active modal based on these enum values.


enum ModalVariant {
  Edit = "Edit",
  Delete = "Delete",
  Hide = "Hide",
  Report = "Report",
  Share = "Share",
}

Enter fullscreen mode Exit fullscreen mode

By implementing this enum, we were able to significantly reduce the complexity and volume of our code. The single state variable, now a flag, indicates which modal (if any) should be open at any given time. This approach not only minimizes the risk of having multiple modals open simultaneously but also simplifies the logic for showing and hiding modals. It marks a substantial improvement in our component's maintainability and scalability, demonstrating the power of thoughtful state management in React applications.

Wrap up

In conclusion, our journey from a component cluttered with multiple useState hooks to a streamlined, single-state system highlights the importance of scalable state management in React applications. The initial approach, while seemingly straightforward, led to increased complexity and potential for errors as new features were added. By adopting an enum to represent modal variants and consolidating state management into a single state flag, we significantly enhanced our component's maintainability and reliability.

This refactor not only reduced the amount of code but also improved the overall architecture of our component, making it easier to understand, extend, and debug. It serves as a valuable lesson in the benefits of anticipating future needs and designing our components with scalability in mind. As developers, it's crucial to continually evaluate and refine our approaches to state management, ensuring our applications remain robust and user-friendly as they evolve.

Through this experience, we've seen firsthand how small changes in how we manage state can have a profound impact on our application's complexity and performance. This encourages us to think critically about our design choices and to embrace best practices that promote efficient and effective state management.

The refactored component.

enum ModalVariant {
  Edit = "Edit",
  Delete = "Delete",
  Hide = "Hide",
  Report = "Report",
  Share = "Share",
}

export default function MoreOptions() {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);

  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  const [modalVariant, setModalVariant] = React.useState<ModalVariant | null>(
    null
  );

  const handleOption = (variant: ModalVariant) => {
    setModalVariant(variant);
    handleClose();
  };

  const hideModal = () => {
    setModalVariant(null);
  };

  return (
    <>
      <Box>
        <IconButton
          aria-label="more"
          id="long-button"
          aria-controls={open ? "long-menu" : undefined}
          aria-expanded={open ? "true" : undefined}
          aria-haspopup="true"
          onClick={handleClick}
        >
          <MoreVertIcon />
        </IconButton>
        <Menu
          id="long-menu"
          MenuListProps={{
            "aria-labelledby": "long-button",
          }}
          anchorEl={anchorEl}
          open={open}
          onClose={handleClose}
        >
          {Object.values(ModalVariant).map((variant) => (
            <MenuItem key={variant} onClick={() => handleOption(variant)}>
              {variant}
            </MenuItem>
          ))}
        </Menu>
      </Box>
      <EditModal
        isOpen={ModalVariant.Delete === modalVariant}
        onClose={hideModal}
      />
      <DeleteModal
        isOpen={ModalVariant.Edit === modalVariant}
        onClose={hideModal}
      />
      <HideModal
        isOpen={ModalVariant.Hide === modalVariant}
        onClose={hideModal}
      />
      <ReportModal
        isOpen={ModalVariant.Report === modalVariant}
        onClose={hideModal}
      />
      <ShareModal
        isOpen={ModalVariant.Share === modalVariant}
        onClose={hideModal}
      />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Feeling enlightened or have thoughts to share? Leave a reaction below and let's get the conversation started. For more insights and updates, follow me on Dev.to. Your engagement helps us create a vibrant community of developers committed to improving our craft.

Happy coding, and see you on Dev.to!

💖 💪 🙅 🚩
marcinzmudka
Marcin Żmudka

Posted on February 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related