17
How to write more readable React code
Written by Chak Shun Yu ✏️
One of the most important aspects of code is its readability. Easily readable code is the result of well-written code, and it has a lot of long-term benefits. It will be easier to read, understand, maintain, review, less prone to unexpected errors, and generally make the lives of other developers a lot easier when they have to interact with that code.
The difficulty of code readability is especially prominent in React development due to its composite nature. The resulting code is filled with a lot of code patterns, very fragmented, and generally distributed over multiple places. This further increases the difficulty of writing readable React code.
However, writing readable React code is not an impossible task. Doing so starts with making the entire process a conscious one. For that, it's important to know what aspects to focus on.
To help you with this, this article will cover multiple topics that you should consider when writing more readable code and its impact on readability, including:
Hopefully, this information will provide you with a solid foundation on how to write more readable React code right now, and in the future.
When discussing code readability, the topic that is most commonly mentioned is the length of the code. Shorter code, in both the vertical and horizontal directions, is often associated with being more readable. The main reason for this is that shorter code equals less code for developers to read through. This results in fewer opportunities that can confuse, which would otherwise make it harder for developers to read through the code.
In reality, however, it's not that clearly differentiated. While writing less code can contribute a lot to readability, it's not an ultimate guarantee. There also comes a turning point where shortening the code even further turns it from being beneficial to harmful for the readability.
When pushing for shorter code with the assumption that it's beneficial for the code readability, the other aspect that is often sacrificed is explicitness.
Take the concept of inline conditional rendering, where it's often between the AND and the ternary operator.
const Component = ({ linkUrl }) => (
<div>
{ !!linkUrl && <PrettyLink url={linkUrl} /> }
</div>
}
// -- OR --
const Component = ({ linkUrl }) => {
return (
<div>
{linkUrl !== undefined && linkUrl !== null ? (
<PrettyLink url={linkUrl} />
) : null}
</div>
);
};
The former is considered shorter and more concise, while the latter is considered lengthy and only appropriate when both branches of the condition are necessary.
But using the && operator
means that one branch is not explicitly stated, so it's up to the reader to figure out what the expected behavior is for the other branch (even if it's to render nothing), whether it was left out by mistake, and look for information that is not provided to them.
This is a clear sacrifice of explicitness for the sake of saving on code length. Whether this is more readable depends on the scenario, but it isn't always as straightforward as "the shorter the better".
One of the reasons we create custom components, Hooks, and functions in React is because it groups related code. Instead of scattering code all over the place, it packages everything in one location under a certain context or purpose.
In the same fashion, the distance at which similar code is grouped also plays a role in the readability.
One of the biggest examples of this occurrence in React development is the introduction of React Hooks. Before Hooks, the only way to include logic with React components was through using class components. To do so, we had to implement lifecycle methods and put pieces of logic in the appropriate places.
Unfortunately, these lifecycle methods were scattered across the component and, in certain cases, were written in a specific order — your logic was broken up and distributed across the component. This increased the distance between related code blocks and often made it hard to see and understand the logic flow.
With the introduction of Hooks, we didn't only receive a way to reuse logic across multiple components, but also a way to group all the related code closely together. This reduced the distance at which similar code is grouped.
This is an important factor for code readability and maintainability, and thus should be kept in mind whenever possible.
In the end, a major part of React development is JavaScript. Implementing React components, logic, Hooks, and more is all done in JavaScript, which means that all of JavaScript can be used for it. That can be both an advantage and a drawback.
As a programming language, JavaScript is very extensive and allows for a lot of different implementations. But a major drawback to such an extensive language is that not everyone will be similarly familiar with all the language details.
Many language features in JavaScript are based on intrinsic details or implicit behavior, which compounds its complexity when coupled with its dynamic nature. These two factors make certain JavaScript constructions more complicated to understand and can negatively impact the readability of your code based on how familiar your developers are with them.
Let’s discuss a few common example JavaScript constructions that I've noticed are more difficult to understand. For all of these constructions, understanding the implicit concepts behind them is crucial for understanding the construction itself. Not having that information can significantly negatively affect readability.
While it's likely that most React developers will be aware of these constructions, it's not a given guarantee, and thus something to keep in mind.
const idObjects = ids.reduce((prev, curr) => {
return {
...prev,
[curr]: {
id: curr,
value: getValueFromId(id),
}
};
}, {});
The Array.reduce
function is often used to convert an array into a different data structure, like an object. The code is very compact, but it's also often difficult to understand — there's a lot of details to keep track of:
- The original array
- The
reduce
call - The previous value
- The current value
- The initial structure
- The resulting structure
- How the different values are combined
The order of this information is also unnatural, like the initial structure being defined last. A different structure that improves upon this is the for-loop. Although it’s considered more ugly and verbose, the resulting code is often more readable due to the more straightforward order of information:
- First comes the initialization of variables
- Second are the length and limits of the loop
- Third come all the actions onto the relevant variables
const Component = ({ hasImage }) => {
// ...
return (
<div>
{hasImage && <Image />}
</div>
);
}
A very commonly used construction for inline conditional rendering is the &&
operator. Based on the value of the left-hand side operand, the right-hand side operand might be rendered.
However, this construction only works due to the implicit JavaScript behavior called short-circuiting. When the &&
expression is evaluated and the left-hand side operator evaluates to a falsy value, then that operand is returned and the evaluation of the right-hand side operand is entirely skipped.
Any given web application will have to deal with all types of information flowing around. Together with the ever-increasing complexity of web applications, it's also never about handling just one data or logic flow. Any UI will have a dozen, hundred, or even a thousand smaller pieces. Every single piece will be connected to some kind of information and have multiple flows going through them.
React provides us with a lot of tools to implement data and logic flows. Think of out-of-the-box Hooks like useState
, useReducer
, useEffect
, and useLayoutEffect
, and the ability to reuse logic in the form of custom Hooks. While these tools allow React developers to handle flows very easily and effectively, they also have their drawbacks in certain scenarios.
It's very easy to entangle a lot of flows in a single location because of how straightforward it is to implement flows into your components. Multiple flows going through a single component or combining pieces of logic from multiple flows into a single useEffect
Hook is not an uncommon occurrence.
const Component = ({ data }) => {
// Logic...
// Here, we're combining flows for the data request, empty state, filled logic state, and
// server error feedback into a single `useEffect`. It feels natural, but is it readable?
useEffect(() => {
if (!data) {
setRequestState("LOADING");
} else if (data.length === 0) {
setRequestState("DONE");
triggerEmptyState();
} else {
setRequestState("DONE");
if (dataIsValid(data)) {
updateOtherLogicWithData(data);
} else {
informServerDataIsInvalid();
}
}
}, [data, updateOtherLogicWithData, informServerDataIsInvalid, triggerEmptyState]);
// Render...
}
The problem with combining piece of logic from multiple flows into a single useEffect
Hook like this is that it can negatively influence code readability. Putting different flows closely together will make them intertwined, difficult to separate, and tightly coupled. The resulting code will thus become more difficult to understand and harder to maintain.
In general, one of the most difficult things in software development is naming things. Proper names can make or break the readability of code. React development is no exception. But due to the composite nature of React, there are a lot of entities to be named. Hooks, components, functions, variables, props, callbacks, contexts — and the list goes on.
Together with the focus on reusability, their names cannot be so specific that they seemingly prevent reusability, but also can’t be too generic because they should reflect their scope and context.
Properly naming them can bring you very far in writing more readable code. Not only does it benefit code readability, but it can also enhance the quality of the code and increase future maintainability. A few examples are:
- Include a prefix with the component's props to indicate the type of the API — this makes it clear to users what behavior is expected of it and how to use it
- e.g., instead of naming a boolean prop
valid
, consider naming itisValid
; instead ofvalidate
, consider calling itonValidate
- e.g., instead of naming a boolean prop
- If you have multiple props that configure a similar aspect, then you can most likely simplify the API design. This can quickly be noticed by the names, like having both an
isLoading
and anisError
boolean prop- Considering an enumeration prop called that captures both of the previous props could make the API design more clear and less cluttered
- Consider the scope in which components can be used — if you have an avatar rendering component that's generic for the whole platform, then it's fine to call it
Avatar
, but if it's specifically meant to be used in a section of a card, then it's beneficial to make that explicit in the name and call itCardAvatar
- Name callbacks after what they do, not how they will be used: if you need to provide a callback to a component's
onChange
prop, then naming that callbackonChange
will not add any useful information to the reader- e.g., instead, consider naming it after what they do:
updateStateValue
increases the readability because it clarifies what the callback does and what will happen when the appropriate change event occurs in the used component
- e.g., instead, consider naming it after what they do:
These are concrete examples of how naming variables differently can change the readability and quality of React code. But it’s not only limited to these examples — the most important thing is to keep this topic in mind when writing, consider the quality and specificity of your naming style, and potentially improve upon it.
There are a lot of different types of code flying around in React development — CSS, JS, HTML (or JSX) — and because of this, a lot of code is located in a single location. Especially in such a UI-centered development field, it means that there will be many scenarios where the code has either minor similarities or differences.
Properly highlighting these occurrences can make a world of difference for the readability of the code. Consider the following:
const SomeSection = ({ isEditable, value }) => {
if (isEditable) {
return (
<OuterSection>
<Header>Edit this content</Header>
<Body>{value}</Body>
<SectionButton>Clear content</SectionButton>
</OuterSection>
);
}
return (
<OuterSection>
<Header>Read this content</Header>
<Body>{value}</Body>
</OuterSection>
);
}
// -- OR --
const SomeSection = ({ isEditable, value }) => {
return (
<OuterSection>
<Header>
{ isEditable ? "Edit this content" : "Read this content"}
</Header>
<Body>{value}</Body>
{ isEditable ? <SectionButton>Clear content</SectionButton> : null }
</OuterSection>
);
}
When these occurrences are highlighted, you can more easily see how certain flows, branches, or results are related to each other, which parts are connected, which parts are different, and so on.
If the approach you use to do this isn’t thoughtful, it can result in duplicate code, or code that requires a higher cognitive load — meaning more things to keep track of, disconnected flows, and harder to understand code.
Writing readable code is one of the most important aspects of software development, and it isn't a trivial task. Its difficulty is especially prominent in React development because of its composite, fragmented, and distributed nature. There is a lot more code and factors to consider when dealing with code readability, which can be overwhelming and make it very difficult.
In this article, I went over various React code aspects to consider when writing more readable code. These include the length of the code, how closely related blocks of code are grouped, whether complicated JavaScript constructions are used, how many flows are handled at once, the naming of your variables and entities, and the degree to which similarities or differences are highlighted. For each topic, we went over why they matter, their impact on the code readability, and how their impact can be controlled.
Using this information, you should have a solid foundation on what aspects to consider and how to write more readable React code. Not only will this have an immediate impact on the readability of your code, but also on its reviewability and long-term maintainability.
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free
17