There are many ways to solve problems in React, some will say too many. In a large code base it is helpful if the developers are united about the way to maintain the code base. Take a look at the topics I present to you in this article, which one do you use and are you and your team on the same page?
React is an unopinionated JavaScript library, meaning that the user can choose freely how to solve a problem. There are no decisions made by the framework on how to specifically write your code. Due to this, I have experienced that many developers have their subjective way of structuring the code. Having a code base with many ways to write and organize your code can be messy and hard to keep track of. Therefore, it is important to have a set of conventions to follow in your codebase.
During my time as a React developer, I have encountered several ways to solve different problems, and personally, I don't like all of them. So, I want to present the five topics I have discussed the most at work.
Take a look at the topics in the article; do you have a preferred way to solve a problem, and are you and your team on the same page?
Named or Default Exports
Let's begin with a simple one: How do you want to make your components available for other components? There are two ways to export your component so you can use it across files: either named exports or default exports. Let's take a look at both of them:
Default exports
When exporting your component as the default, you can export a single value from that given module. You can only export one component from a file as the default; the other exports must be named if you want to make more components available from the same file. Take a look at the example of exporting a component as the default:
function MyComponent() {}
export default MyComponent
How you export it will tell you how you must import it. And when importing a component that is exported as the default in another file, you can name it the same as the name of the component where you are importing it:
import MyComponent from './MyComponent'
Personally, I think the syntax for importing this is clean, not too many curly brackets and clutter for the human eye; you only export one value from a given module (which is nice, structuring your components in separate files). However, you can also import and give the component whatever name you want, like this:
import Banana from './MyComponent'
Giving the user (aka the developer) the possibility to name the import as they want may lead to inconsistencies between the export and the import. Another downside to this approach is if you want to rename your file. If you rename your component and forget to update the imports, you quickly get a mismatch between the component name and the imported name. It may become a real head-scratcher for future teammates searching for the component when debugging.
Named exports
When exporting your component as a named export, the name has to match when you export it and when you import it.
Exporting a named export:
export function MyComponent() {}
Importing a named export
import { MyComponent } from './MyComponent'
Using named exports will make renaming much easier. Giving your component a new name will force you to remember to rename the imports since refactoring components gives the opportunity to automatically update the references or give you a TypeScript error.
Naming of files
Let's say you want to develop a new feature, and you are creating the file that will be the main component for this feature. What do you call it? I have practiced both naming it index.tsx but also experienced the advantages of a more descriptive filename as CoolFeature.tsx.
Calling files index.tsx
In my previous project, we always called the main file index.tsx, as it makes it easy to see what was the "entry point" for this feature when you are looking in a given folder.
Example of a feature with an index.tsx file as the entry point:
src
|
+-- CoolFeature
|
+-- Header.tsx
|
+-- index.tsx
|
+-- List.tsx
When quickly scanning content of the "CoolFeature" folder, you can see which file you can open to begin exploring the feature. However, searching for index.tsx in a global search in your very large code base will result in tens or even hundreds of occurrences in the search result.
Same name as the folder
Another way is to have the entry file having the same name as the folder and the feature it represents. From the example above with the "CoolFeature" folder, we can have a more descriptive filename like CoolFeature.tsx instead of index.tsx:
src
|
+-- CoolFeature
|
+-- CoolFeature.tsx
|
+-- Header.tsx
|
+-- List.tsx
It is not as easy to scan as index since the name will be different in every folder, but on the other side, it is easy to search for as it (hopefully) results in a singular match across the code base. It is also important to remember to rename the entry file as well as the folder name when refactoring features and giving them new names.
Naming of Props
Most components have props since it is the most common way to share data between your components. Since we live in 2023, it is common to use TypeScript to ensure type safety for your component, but what are you naming the type for the props?
You have two options: either name it just Props or prefixing it with the name of the component it belongs to.
How it will look like when prefixing the prop interface with the name of the component:
interface ThisIsAComponentProps {
// ...
}
function ThisIsAComponent(props: ThisIsAComponentProps) {}
Naming the interface as just Props
interface Props {
// ...
}
function ThisIsAComponent(props: Props) {}
Both cases have their pros and cons as I see it. By naming it Props, it is easy to scan in a component, and you avoid line breaks. However, searching in a codebase after the word "Props" will result in many matches. Further on, if you want to export the type you will get name clashes since all your objects are named Props. (This can easily be solved by giving it another name when importing it: import { Props as MyComponentProps }
). By prefixing Props, it is very easy to search for, but it drowns in long variable names and results in long lines (I don't like long lines). I also feel it is redundant naming of your types.
Bonus quarrel: Do you define the props over or under the component?
Regular Functions vs. Arrow Functions
Functions are essential when creating a React application, as they are the building blocks of React. Mainly there are two ways to define a function in JavaScript: either by using regular functions or using arrow functions.
Writing your component as a regular function is a known way to write a function and was the only way to write functions until ES6 was released. Take a look at the example:
function MyComponent() {...}
Writing your component with the arrow function syntax is more terse and might, for some people, be more readable.
const MyComponent = () => {...}
It is also nice when you want a short hand for returning a single expression statement:
const MyComponent = () => <AnotherComponent />
A functional difference for these two ways to write a function is the meaning of this
. (In regular function, this
changes according to the way that function is invoked; read more about it here). However, after the release of React 16 with functional components, the majority of React developers don't need to be concerned about this
.
Bonus: In addition to deciding on how to declare your React components, what about the helper functions inside the component?
function MyComponent() {
// Declaring the function as:
const onClick = () => {}
// or
const onClick = function() {}
return (
// ...
)
}
Ternary Operator or Early returns
The ternary operator is a conditional operator in JavaScript that allows you to return one value if the condition is true and another return if the condition is false. It is kind of a shorthand for if/else. In the simplest way, you can have a ternary operator which returns some variables based on a condition:
myBooleanValue ? value1 : value2
It is also nice to use it for conditional rendering; take a look at the example below which returns a component instead.
return myBooleanValue ? <MyComponent /> : <MyOtherComponent />
Another way to return a component is in the traditional if/else:
if (myBooleanValue) {
return <MyComponent />
}
return (
<MyOtherComponent />
)
Ternary operators are nice, and I use them frequently as they often result in fewer lines of code. However, I have experienced that the code can be hard to read with a ternary operator in the middle of a big component. Especially if the expression breaks over several lines (and yes, part of the problem may be that the component is too big). A way to break out of big return statements is early returns.
It may be confusing to have several return statements inside a component when debugging what that component actually returns.
Hope you have gathered some arguments you can bring to your team. Happy arguing!