How to make your React code more readable and maintainable

Quentin Duval
Legalstart

--

We all struggled trying to understand someone else’s code, or even our own, after some time. This can lead to consequent wastes of time. Here are some tips to make your React code more readable and understandable. This article is mainly written for React beginners, but everyone might find something interesting!

Comments should not be necessary

I have been taught at University that good code was commented code, which I think is wrong. Comments actually make me feel that the code will be complicated and has not been structured correctly. It is often better to split your code into smaller pieces with explicit naming than commenting a bigger piece of code that does many things at once.

Here is an example.

The type Person is an object containing two fields: id and name, both strings. We are implementing a function sayHello, taking a person id as parameter and logging "hello" with the person name, in upper case.

type Person = {
id: string;
name: string;
};
const people: Person[] = [
{ id: '1', name: 'Chesterfield Outerbridge' },
{ id: '2', name: 'Mergatroid McFadden' },
{ id: '3', name: 'Longbranch Vinaigrette' }
];
sayHello(people, '1'); // logs "HELLO CHESTERFIELD OUTERBRIDGE!"

If we wanted to do everything at once, the function would be implemented this way:

const sayHello = (people: Person[], id: string): void =>
console.log(`HELLO ${people.find((person) => person.id === id).name.toUpperCase()}!`);

Even for such a simple example, having a unique function handling everything makes it much harder to read. Instead, we should split each step in a unique variable or function.

const getPersonById = (people: Person[], id: string): Person =>
people.find((person) => person.id === id);
const sayHello = (people: Person[], id: string): void => {
const person = getPersonById(people, id);
const helloString = `hello ${person.name}!`;
const upperCaseHelloString = helloString.toUpperCase();
console.log(upperCaseHelloString);
};

This process is obviously “over-engineering” for this simple example, but it is a good way to keep your code well-structured and easily readable. It also makes it easier to test.

Create new functions when needed

New functions should be created in two cases:

  • to structure your code and make it easier to read and test (as mentioned previously)
  • to avoid code duplication

The new function can be placed in the same file if it only makes sense in this file context, or it can be placed in a helper if used across the application.

The same can be applied to components — it will let you factorize the style and behaviors and make the application easier to update. However, make sure that the factorization makes sense in your case. It can happen that two components have the same behavior or styling at one moment, but are not really bound.

Creating new function can also be useful to condition a variable. In this example, we want to create a function to say “hello” in different languages.

type Language = 'English' | 'French';const sayHello = (language: Language) => {
const helloString = language === 'English' ? 'Hello!' : 'Bonjour !';
console.log(helloString);
};

We can keep code readable in one function as long as we have only two languages proposed, thanks to ternary operator, but what will happen if we add a third language?

type Language = 'English' | 'French' | 'Spanish';const sayHello = (language: Language) => {
let helloString;
switch (language) {
case 'English':
helloString = 'Hello!';
break;
case 'French':
helloString = 'Bonjour !';
break;
case 'Spanish':
helloString = '¡Hola!';
break;
default:
helloString = '';
break;
}
console.log(helloString);
};

We need to create a variable and change its value. In this case, creating a function to condition the helloString variable would make the code more readable. This is made possible thanks to the return keyword that will make the function end, which will avoid multiple break or else .

type Language = 'English' | 'French' | 'Spanish';const getHelloString = (language: Language) => {
switch (language) {
case 'English':
return 'Hello!';
case 'French':
return 'Bonjour !';
case 'Spanish':
return '¡Hola!';
default:
return '';
}
};
const sayHello = (language: Language) => {
const helloString = getHelloString(language);
console.log(helloString);
};

Naming is important

Across my career, I have seen many variables without explicit names, like array , or even worse. This makes waste time to whoever will read your code. Variable naming is very important, and using time to find the right name is a good investment.

5 things to know when you name a variable

  1. Do not use type styled name: array, string, etc
  2. Give explicit names, even if they can seem too long
  3. Shorten name within the context. For instance, within a Section component, title can be named title instead of sectionTitle . This works when the variables are not exported.
  4. Respect naming conventions. Boolean names should start with “is” or “has”.
  5. Boolean names should not include negations. For example, isNotDisabled could lead to confusion, especially when its negation will be used (!isNotDisabled ). This can be easily avoided by using an affirmative assertion (isEnabled or isDisabled ).

How to name your components

  • Each component should be in its own file
  • Its file should have the same name as the component, in Camel Case, and starting with uppercase

Example: A help panel component would be named HelpPanel, in a file named HelpPanel.tsx

Types are a guide, not a constraint

Types can be seen as a constraint while developing, but you can use them to guide you and make your code and thinking more robust. You can do it by typing every variable and function before their implementation. Thinking about function returning type forces you to be sure about what the function does and about its naming.

It will help your IDE to autocomplete and find errors in your code.

If we add a typo to our previous example, it will be easily detected thanks to typings.

type Language = 'English' | 'French' | 'Spanish';const getHelloString = (language: Language) => {
switch (language) {
case 'Englsh': // Type '"Englsh"' is not comparable to type 'Language'.
return 'Hello!';
case 'French':
return 'Bonjour !';
case 'Spanish':
return '¡Hola!';
default:
return '';
}
};

Here, the IDE will help you to autocomplete the different values that language can take, and it will display an error if a wrong language has been typed (Type '"Englsh"' is not comparable to type 'Language'. )

It can happen that types are too complex to implement right away. In this case, you can keep theany type until you feel ready to create the right type.

Conclusion

Of course we can do more, but if you follow these guidelines, most of your code should be clean and readable by others.

I hope that these few tips will help you to produce better code and will guide you during your future implementations!

--

--