DEV Community

Kosuke Ri
Kosuke Ri

Posted on

A few weeks maintaining React components, using various approaches

Context

This post is a short made-up, based on a real experience. It aims to show different approaches to developing and maintaining React components in an application.

In order to use react-select in a french government project, we have to translate placeholders and other default options. As far as I know, there is no i18n extension for react-select; anyway, it may not have fit our need since there may be some domain-specific text. Here we go, let's use the component's props:

<Select
  loadingMessage={() => "Chargement…"}
  noOptionsMessage={() => "Aucun résultat"}
  placeholder="Choisissez une valeur"
  // …
/>
Enter fullscreen mode Exit fullscreen mode

It works.

👉 Now, we want to re-use this component with same options elsewhere, then everywhere else.

Week 1 - Create a custom select component

We could create a MySelectthat has some default options. Since the only thing that changes for each instance are the name and the change handler, we need two props:

export default function MySelect({ name, onChange }) {
  return (
    <Select
      loadingMessage={() => "Chargement…"}
      noOptionsMessage={() => "Aucun résultat"}
      placeholder="Choisissez une valeur"
      name={name}
      onChange={onChange}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Thus, we could use it in various files and components:

<MySelect name="user" onChange={(v) => setUser(v)} />


<label>Liste des projets</label>
<MySelect 
  name="project" 
  onChange={(v) => setProject(v)} 
/>


// Another file
<MySelect name="foo" onChange={bar} />
Enter fullscreen mode Exit fullscreen mode

It still works, it is factorized, well done!

In order to add new options we could add new properties.

export default function MySelect({ 
  name, 
  onChange, 
  value, 
}) {
Enter fullscreen mode Exit fullscreen mode

Then…

export default function MySelect({ 
  name, 
  onChange, 
  value, 
  customStyles = [],
}) {
Enter fullscreen mode Exit fullscreen mode

And so on…

export default function MySelect({ 
  name, 
  onChange, 
  value, 
  customStyles = [],
  disabled = false,
  // etc.
}) {
Enter fullscreen mode Exit fullscreen mode

🙅 OK, let's refactor it! It does not scale anymore. ❌

Week 2 - Passing props down to a custom component

Since adding properties seems like a useless layer over the MySelect component, let's just pass props to the component thanks to spread operator:

export default function MySelect(props) {
  return (
    <Select
      loadingMessage={() => "Chargement…"}
      noOptionsMessage={() => "Aucun résultat"}
      placeholder="Choisissez une valeur"
      {...props}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

We just removed some complexity, that seems easier to maintain.

Week 3 - Specialization

A few days later, we figure out that we are repeating code by writing the same sub-component again and again:

<MySelect
  isSearchable
  isClearable
  isMulti
  name="products"
  // …
/>

<MySelect
  isSearchable
  isClearable
  isMulti
  name="services"
  // …
/>

<MySelect
  isSearchable
  isClearable
  isMulti
  name="things"
  // …
/>
Enter fullscreen mode Exit fullscreen mode

It seems many MySelect needs the same 3 attributes: isSearchable, isClearable, isMulti. We could create a new component that looks like MySelect:

export default function MySearchableSelect(props) {
  return (
    <Select
      loadingMessage={() => "Chargement…"}
      noOptionsMessage={() => "Aucun résultat"}
      placeholder="Choisissez une valeur"
      isSearchable
      isClearable
      isMulti
      {...props}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Still, MySearchableSelect has some code in common with MySelect so maybe we could consider MySearchableSelect should render a specific version of MySelect (specialization):

export default function MySearchableSelect(props) {
  return (
    <MySelect
      isSearchable
      isClearable
      isMulti
      {...props}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

This approach seems compatible with the DRY principle. However, it makes the code harder to debug after a few more days. When we have a problem in a code that uses MySearchableSelect component, we may have to jump to MySearchableSelect, then MySelect, then Select and back again to understand where the problem is and to choose where to fix it. The code is oversimplified in this example: in a real-world example, it could take some time to debug. There is also a risk to create MyCreatableSelect, MySearchableAndCreatableSelect and so on. As a side note, using DRY as a main principle could be considered harmful.

Maybe we could remove abstraction.

Week 4 - Adding conditions

To avoid this complexity, we could get rid of MyCreatableSelect, then add a boolean prop to MySelect that allows handling the case where MySelect is a specialized component:

export default function MySelect({ 
  searchable = false
  ...props
}) {
  let searchableProps = {};
  if (searchable) {
    searchableProps = {
      isSearchable: true,
      isClearable: true,
      isMulti: true,
    };
  }
  return (
    <Select
      loadingMessage={() => "Chargement…"}
      noOptionsMessage={() => "Aucun résultat"}
      placeholder="Choisissez une valeur"
      {...searchableProps}
      {...props}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)