How do you keep React components from becoming giant, tangled blobs?

Creating reusable components is easy with this React pattern.


It's easy to find ourselves trying to design a generalized, reusable component. But one new requirement often turns a clean component into a tangle of booleans. Here is how composition can help.

"When updating, I want to not have attachments enabled. Let me create a boolean for that."One new requirement or an additional use case and the whole thing needs reworking. For example, the most common problem surrounds reusing a similar component for creating and updating an entity (e.g., products, user details, etc.). The straightforward way is to duplicate the form and refactor for the changes. One level higher, we try to introduce booleans.I'm all too familiar with isEditing or isUpdating booleans. This solves the problem... for the short term. Inevitably, there will be another context in which using this component makes sense with minor tweaks. Let's say you want to add drag-and-drop functionality when a user is creating their profile, but not when they are editing it. The component becomes unmanageable by adding just one requirement.

const UserForm = ({ isEditing, onSubmit }) => {
  return (
    <form onSubmit={onSubmit}>
      <label>Name</label>
      <input placeholder="Kranthi Sedamaki" />

      <label>Phone Number</label>
      <input placeholder="9876543210" />

      <label>Email</label>
      <input placeholder="someone@test.com" />

      {/* boolean here */}
      {isEditing ? null : <Dropzone />}

      {/* boolean again, checking for conditions */}
      <button type="submit">
        {isEditing ? "Update" : "Create"}
      </button>
    </form>
  );
}

This is how components blow up and become unmanageable. What if you can prevent that? The cleanest, most elegant, and powerful design pattern I've found for React is composition. Instead of conditional checks for every use case, we develop reusable primitives. Then, we can use what we want and ignore the others. Think of every element in your component as a primitive. For example, a message field can have a header, a footer, and content. Each of these fields can have any combination of widgets.

Returning to our problem of adding a drag-and-drop feature, you can simply compose the component for the specific context you need.

// Reusable primitives
const UserFormLayout = ({ children, onSubmit }) => (
  <form onSubmit={onSubmit}>{children}</form>
);

const UserInputs = () => (
  <>
    <label>Name</label>
    <input placeholder="Kranthi Sedamaki" />
    <label>Phone Number</label>
    <input placeholder="9876543210" />
    {/* ...other inputs */}
  </>
);

// Context 1: Creating a user (includes Dropzone)
const CreateUserFlow = () => (
  <UserFormLayout onSubmit={handleCreate}>
    <UserInputs />
    <Dropzone />
    <Button>Create</Button>
  </UserFormLayout>
);

// Context 2: Editing a user (No Dropzone)
const UpdateUserFlow = () => (
  <UserFormLayout onSubmit={handleUpdate}>
    <UserInputs />
    <Button>Update</Button>
  </UserFormLayout>
);

No booleans necessary!

Check out this great talk from React Universe Conference, Composition is all you need, for a specific example on how Slack implemented their messaging forms.