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"
// …
/>
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}
/>
);
}
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} />
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,
}) {
Then…
export default function MySelect({
name,
onChange,
value,
customStyles = [],
}) {
And so on…
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:
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:
<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
:
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):
export default function MySearchableSelect(props) {
return (
<MySelect
isSearchable
isClearable
isMulti
{...props}
/>
);
}
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}
/>
);
}
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";
}
// ...
}
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,
}
Then we could use it and repeat it here and there.
<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.
Top comments (0)