sinaptia waves

Don't build lasting relationships with your components

Enzo Beducci
May 24th, 2023

Note: Some code examples will be based on React. However, the concepts and patterns are framework-agnostic.

Nothing is forever

When working on a project, it’s very common to build components that accomplish a specific purpose, following our client’s instructions. However, as new requirements emerge, we often need to introduce small changes that may eventually cause the component to stop functioning. There comes a point where it is easier to create a new component rather than reusing what we already have. So, how can we avoid this from happening?

Early stage

To address this issue, we can take as an example a simple and widely used component called Input. At the beginning of the project, our client informs us that it will have two variants: standard and outlined, and some text as a placeholder. As good-intentioned frontend developers, we probably will create something similar to this:

<Input
  value={value}
  onChange={updateState}
  placeholder='Text as placeholder'
  variant='standard'
/>

For now, our Input meets all the requirements and seems easy to use anywhere in our application. With no more than 5 lines of code, we have ourselves a functional component.

Now, the client needs a search icon to appear on the left side within the input in some areas of the application. This doesn’t seem to complicate things too much; we can add a new prop that indicates the icon to use and the component will take care of it:

<Input
  value={value}
  onChange={updateState}
  placeholder='Text as placeholder'
  variant='standard'
  icon='search.svg'
/>

Alright, this solves our problem and the client is satisfied.

After showcasing the product, the designers realize that some of the inputs need to indicate the currency with a label on the right side, such as USD for example. To address this, we can add a new prop that indicates the currency.

<Input
  value={value}
  onChange={updateState}
  placeholder='Text as placeholder'
  variant='standard'
  icon='search.svg'
  currency='USD'
/>

Once again, disaster was averted.

Things start to pile up

As we can see, these changes may seem minimal for each interaction we make on our component, but we start to notice the complexity growing.

Now, after showcasing the product to potential buyers, our client listens to the feedback from the entire team and decides that, as a user, it would be good to be able to choose the currency within the same Input. In other words, create a kind of selector where we could select the currency.

This already implies new functionality where, although there are different approaches to tackle it, as a quick and dirty way to get the component out as soon as possible, we add a “selector” as a prop with all the available currencies.

<Input
  value={value}
  onChange={updateState}
  placeholder='Text as placeholder'
  variant='standard'
  icon='search.svg'
  currencySelector={['USD', 'EUR', 'ARS', 'UYU']}
/>

It seems to work, and what the UI doesn’t understand couldn’t harm, right?

Well, the problem here is that the code starts growing in a way that adapting to new requirements now can feel like a mountain we have to climb.

Let’s imagine for example that on a new page, we need to use the Input only as a password input. What do we do with all the other props that we won’t be using? We can simply ignore them and not use them, but the component should be prepared for these cases and handle each interaction accordingly.

A little too late

Let’s suppose that as the next requirement, the icon of the Input should adapt to the selected currency. So, if I choose EUR, I should expect to see “€” at the beginning of the input.

Should I send the symbols for each currency in a separate prop and display them based on the selected currency?

What about the “search.svg” icon that we integrated at the beginning?

At this point, with new requirements, we find ourselves with these options:

  • Trying to integrate the selector and updating the icon within the same component (this would involve many conditionals and complicated logic).
  • Separating the Input from other types of inputs by calling it InputCurrencySelector. This will result in duplicating style logic in different parts of the application.
  • Effectively killing the Input component and creating a NewInput.

For all of these cases, we introduce technical debt and the onboarding curve grows.

What happened along the way?

What happened here is that we developed an emotional attachment to our component. We grew fond of it from the beginning and we don’t want to let it go. Instead, we are trying to make it survive through the design’s changing challenges. Adapting it in a way that fulfills all new requirements.

Like any toxic relationship, we must let it go.

Let’s think, what is the actual purpose of an input? Thinking from the HTML side, the only thing we should obtain from an input is the information that the user provides. Nothing more.

An Input component should specifically focus on that, without worrying about icons or selectors.

The major problem that comes from this bad relationship with your component lies in the fact that props predominantly shape the content rather than the component’s intrinsic nature.

The content of the Input should not be defined by props. If we had kept the core responsibilities of the HTML input element in mind, the structure of our Input would have immediately been different.

<Input
  value={value}
  onChange={updateState}
  variant='standard'
/>

Note: we kept the variant as it is defined on the component itself.

If we need to add an Icon (whatever it may be), we can do so independently of our Input component.

<Input
  value={value}
  onChange={updateState}
  variant='standard'
>
  <Icon
    name="search.svg"
  />
</Input>

If we need the selector as well, we can work on it in a separate component.

<Input
  value={value}
  onChange={updateState}
  variant='standard'
 >
  <Icon
    name={selectedCurrency}
  />
  <CurrencySelector
    onCurrencySelect={(currency) => setSelectedCurrrency(currency)}
  />
</Input>

By doing so, we divide the responsibilities of each component and reduce them to their essential tasks:

  • Icon displays an icon
  • CurrencySelector is only concerned about with returning a selected currency
  • Input handles user input data

Additionally, we can add a <Box> element to properly position the elements.

If we want, we could also isolate this component separately as CurrencyInput and reuse it everywhere. However, in doing so, we limit its use to this specific purpose.

If you love them, let them go

It’s important not to become too attached to our components. Often, we ask too much from them and burden them with responsibilities that exceed their core function. We treat them as heroes who must handle everything and always adapt to new needs. It’s important to isolate functionalities, making our components more flexible and ready to adapt to any changes that may happen.