Good code is like a love letter to the next developer who will maintain it.
When I saw this quote from Addy Osmani, it immediately clicked for me. While working I spent 70-80% of the time reading existing code. This ratio is more or less the same for every developer.
Now, for a code to be good it needs readability. In this post, I will try to cover how we can write readable code in React.
Some of the contents of this post are opinionated. You might have a different opinion. Differing opinions lead to better discussion and progress
Components Name
Name should be describing what it does concisely
Imagine, a page "/orders" where we can find:
- All orders with some filters and pagination in a table.
- A column called "Action", which is a drop-down. There is a text called "Customer Details", clicking that opens a modal with customer details.
The page component name is straightforward for the given example. <Orders/>
, <OrdersList/>
or <AllOrders />
.
"Orders" or "OrderList" both are fine. "AllOrders" is a bad choice because developers will likely search the code base with the letter "O". So, it will be hard to find.
"There is a modal that shows customer details for a particular order."
This sentence is a short description of the product we will be building. So the modal name should be "CustomerDetails" as derived from the product description. This should be done always so that component names are relevant throughout the application lifecycle and to other teams as well.
Components Order
The parent/exported component of the file should be the first thing inside the file, after all the import statements
Think parent/exported component is like a story. We don't find the middle of the story snippet on the first page or the first line. It follows an order. Every reader is familiar with the order of the story.
Imagine, I have to add a column called "Date" in the table. From the design, I got the HTML markup order.
- Page Title
- Download the invoice on the left
- Upload the invoice on the right
- Invoice list table
The first thing after the import statement is the "Upload invoice" component. The HTML markup order that I had does not match the order of the components. So, at first glance, I am not 100% sure if is this the correct file. Now I have to scroll down to find where the main component is. Then I will find the "Table Component" and make the necessary changes.
If the order of the components matched the HTML markup, it would have been more readable.
Start Component with JSX
From the previous example, my task is to add a "Date" column in the table.
Most components consist of API calls, states, and callback functions. Normally, we write all the JavaScript parts first, then write the JSX.
For this task, I don't need to read or scan all the JS parts of this component. I have to first find the table, and add the column. Then I have to make changes to the state responsible for showing dynamic data to the table.
function ComponentWithDataLayerHook() {
const { state, onSubmitFilterFormSubmit } = useTableDataLayerHook();
return (
<div>
<PageTitle />
<TableFiltersForm onSubmit={onSubmitFilterFormSubmit} />
<Table data={state.tableData} />
</div>
);
}
By introducing a data layer hook we can achieve that. API calls, states, and on-click handlers will be on that hook. The HTML markup I saw on the design, is now easily discoverable on the code. I can easily find the place to add the new column and the state that is responsible for showing data in the table.
Handle edge case on the child component
The above image is a comment showing components. Imagine this a comment section for a blog post. Let us think about edge cases for this component.
- As the comment is optional, there is a possibility of no comments for a particular post.
- There might be lots of comments, so we have to show a spinner while comments are loading.
We can handle these edge cases in two places. Parent component or the child "Comment Component".
If we decide to handle these edge cases on the parent component below scenarios will come
- The parent component will consist of other components that might have these sets of edge cases. If we handle all of the edge cases on the parent component, it will be complex and hard to read.
- Component behavior is split between both the parent and the child component. It will be hard to read and understand the whole features of the component.
So, if we handle these scenarios in the child component both parent and child component readability will increase.
Use useReducer
Let's see how many states are needed for this data table component.
These states will get the job done but with some drawbacks.
- States that are related to each other are hard to understand. The 5th state (table data) depends on the 2nd, 3rd, and 4th states (selectedDropdownItem, startDate, endDate). If we group these states in one single state, relations will established.
- Component logic is hard to understand as event handlers and states updated are not separate.
- If one more state (e.g. status) is added to the component, then we have to handle that in the
onReset
andonSubmit
. So, the complexity of the component logic is growing.
Let's convert this into useReducer
At first glance, there are lots of codes for state management. But state update logic and what event handlers are doing separated. So, it's easier to read and understand the logic of state updates.
If more state is added complexity of the state management will not grow.
Avoid ternary operator in large JSX block
We need to add one more <p/>
tag on the else block condition1
. As this code block is large, it's difficult to follow the else block. So, to avoid this we can do a few things.
- Use
return early
technique. (The next section will discuss more) - If
return early
is not possible in the parent component, break this whole block of code into a separate component. Then use thereturn early
pattern.
Avoid chaining the ternary operator. This is also hard to read as there are multiple levels of indentation.
Instead, we can use the same return early
pattern.
Now indentation is one level and we can read this linearly.
Return Early
Return early is the way of writing functions or methods so that the expected positive result is returned at the end of the function and the rest of the code terminates the execution when conditions are not met.
This is a simple comment component with 2 negative results. All the negative cases are handled inside the default return.
Let's convert this with return early
pattern.
Using this pattern we can achieve single-level indentation hence it's more readable.
A reference blog post link is added about this pattern at the end.
Move multiple conditions checks from JSX to function
Sometimes we have to check multiple conditions to render one element.
{var1 === 'some string' && var2 === false && var3 && (
<div
Show me
</div>
)}
We can put all the conditions inside a function and return a boolean to check for rendering the component. Now, logic is separated from the JSX.
const showDiv = () => {
if (var1 === 'some string' && var2 === false && var3) {
return true;
}
return false
}
return (
<>
{showDiv() && (
<div>
Show me
</div>
)}
</>
Writing readable code is crucial for effective software development. It leads to better collaboration, faster debugging, and overall efficiency. We should write codes for humans first, and machines second.
Thank you for reading this through. Please share your feedback in the comments section.
Top comments (0)