A few weeks maintaining React components, using various approaches
Tags: react, javascript, programming
Published: 2021-12-13 22:37:30

## 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](https://react-select.com/) 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:

```react
<Select
  loadingMessage={() => "Chargement…"}
  noOptionsMessage={() => "Aucun résultat"}
  placeholder="Choisissez une valeur"
  // …
/>
```

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:

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

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

```react
<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} />
```

It still works, it is factorized, well done! 

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

```jsx
export default function MySelect({ 
  name, 
  onChange, 
  value, 
}) {
```

Then…

```jsx
export default function MySelect({ 
  name, 
  onChange, 
  value, 
  customStyles = [],
}) {
```

And so on…

```jsx
export default function MySelect({ 
  name, 
  onChange, 
  value, 
  customStyles = [],
  disabled = false,
  // etc.
}) {
```

🙅 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](https://reactjs.org/docs/jsx-in-depth.html#spread-attributes):

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

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:

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

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

<MySelect
  isSearchable
  isClearable
  isMulti
  name="things"
  // …
/>
```

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

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

Still, `MySearchableSelect` has some code in common with `MySelect` so maybe we could consider `MySearchableSelect` should render a specific version of `MySelect` ([specialization](https://reactjs.org/docs/composition-vs-inheritance.html#specialization)):

```react
export default function MySearchableSelect(props) {
  return (
    <MySelect
      isSearchable
      isClearable
      isMulti
      {...props}
    />
  );
}
```

This approach seems compatible with the [DRY principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). 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](https://overreacted.io/the-wet-codebase/).

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:

```react
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}
    />
  );
}
```

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:

```react
export default function MySelect(/* ... */) {
  let searchableProps = {};
  if (searchable) {
    if (something) {
      // ...
    }
    // ...
  } else if (!name) {
    name = "defaultName"; 
  } 
  // ...
}
```

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:

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

export const searchableProps = {
  isSearchable: true,
  isClearable: true,
  isMulti: true,
}
```

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

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

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

<Select name="inEnglish" {...searchableProps} />
```

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.