DEV Community

Rap2h
Rap2h

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 MySelect that 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

It has less abstraction than the previous approach, still it has more cognitive complexity. For now, it's still easy to read, but it will be harder and harder to maintain due to the potential proliferation of specific cases:

export default function MySelect(/* ... */) {
  let searchableProps = {};
  if (searchable) {
    if (something) {
      // ...
    }
    // ...
  } else if (!name) {
    name = "defaultName"; 
  } 
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Also, changing a condition could have a side-effect on any MySelect component that could be hard to predict without a lot of tests.

What if we actually do not need to create a custom component?

Week 5 - Create shared default properties

Since creating a new component MySelect is for sharing properties, we could create exported properties that we could use in various places:

export const frenchProps = {
  loadingMessage: () => "Chargement…",
  noOptionsMessage: () => "Aucun résultat",
  placeholder: "Choisissez une valeur",
}

export const searchableProps = {
  isSearchable: true,
  isClearable: true,
  isMulti: true,
}
Enter fullscreen mode Exit fullscreen mode

Then we could use it and repeat it here and there.

<Select name="users" {...frenchProps} />

<Select name="teams" {...frenchProps} {...searchableProps} />

<Select name="inEnglish" {...searchableProps} />
Enter fullscreen mode Exit fullscreen mode

There are some advantages in using this approach:

  • There is no abstraction layer over the base component.
  • There is no cognitive complexity.
  • There is no home-made hard-to-maintain component.
  • We are using the actual react-select library.
  • Code is more boring, no need to be smart.

Conclusion

I'm sure there are other approaches than the ones I've described in this short post. Nevertheless, this is more or less what I've seen in the React repositories I've browsed or contributed to. I'm also sure there are hidden pitfalls in the "last week" approach and omitted benefits in the first weeks' approach.

I may have missed some approaches, though! I would be glad to hear feedback on other people's chosen approaches.

Top comments (0)