Best Practices for Clean React Apps
React is a front-end framework and one of the most popular JavaScript libraries for building user interfaces on the web. With its diverse features, it provides a lot of flexibility to create a wide variety of applications. We may be writing code and building great UIs that are working. However, writing code just to create apps isn’t enough; the code should be clean and readable to help us develop maintainable apps. Clean code is not just code that works, but rather code that can be easily read, reused, refactored, extended, and scaled by others. Hence, we should write code in a manner that should be self-explanatory, easy to read and understand, and help in ensuring consistency.
This blog aims to present a compiled version of some widely accepted writing styles and practices to keep in mind while working with React that should be helpful to both intermediate and experienced developers.
1. Use Object Destructuring:
Object destructuring allows you to take specific fields from an object and assign them to a variable instantly. It reduces the number of code lines we need to extract the object properties and makes your code easier to understand. Object destructuring saves a great deal of explicit variable declarations, and it is really useful in situations when:
- Using multiple properties from an object.
- Using the same property multiple times.
Refer to the example below on how to use destructuring for a case where we need to show a user’s details. Using multiple properties from an object.
- Using the same property multiple times.
Refer to the example below on how to use destructuring for a case where we need to show a user’s details.
// ❎
<div>{user.name}</div>
<div>{user.age}</div>
<div>{user.profession}</div>
// ✅
const { name, age, profession } = user;
return (
<>
<div>{name}</div>
<div>{age}</div>
<div>{profession}</div>
</>
);
2. Props Destructuring:
The above approach of object destructuring can also be leveraged while accessing props.
// ❎
const NavBar = (props) => …
// ✅
const NavBar = ({ showNotifications, showProfile, }) => …
3. Array Destructuring:
An often overlooked ES6 feature is array destructuring. It allows us to access array values without specifying indexes.
// ❎
const splitLocale = locale.split('-');
const language = splitLocale[0];
const country = splitLocale[1];
// ✅
const [language, country] = locale.split('-');
4. Use Template Literals:
Use template literals to build large strings. Avoid using string concatenation. It’s nice and clean.
// ❎
const userDetails = user.name + "'s profession is" + user.profession;
return <div>{userDetails}</div>;
// ✅
const userDetails = `${user.name}'s profession is ${user.profession}`;
return <div> {userDetails} </div>;
5. JSX Short Hand:
Use JSX shorthand for passing explicit true boolean props to Components. For example, if you want to make your title visible in your NavBar Component:
// ❎
<Navbar showTitle={true} />
// ✅
<Navbar showTitle />
6. Use Ternary Operators:
For cases where we know that we expect only 2 possible options, use ternary operators. Let’s say you want to show a particular user’s details based on role.
Notice in the following examples how the above code causes the equality check to be evaluated 2 times. While with ternary, we can lower it to once.
// ❎
const { role } = user;
return (
<>
{role === ADMIN && <AdminUaser />
{role !== ADMIN && <BasicUser />
</>
);
// ✅
const { role } = user;
return role === ADMIN ? <AdminUser /> : <BasicUser />;
7. Take Advantage of Object Literals:
Object literals can help make our code more readable. Let’s say you want to show three types of users based on their roles. Using ternary is not suggested because the number of options is greater than two (it would need nesting). Object literals can be used instead.:
// ❎
const { role } = user;
switch (role) {
case ADMIN:
return <AdminUser />;
case EMPLOYEE:
return <EmployeeUser />;
case USER:
return <NormalUser />;
}
// ✅
const { role } = user;
const components = {
ADMIN: AdminUser,
EMPLOYEE: EmployeeUser,
USER: NormalUser,
};
const Component = components[role];
return <Componenent />;
In addition to increasing readability in such scenarios, we can also employ object literals to map dynamic or computed properties.
8. Use Fragments:
Always use Fragment over div
or span
as containers. It keeps the code clean and is also beneficial for performance because we are not adding useless containers to the DOM tree, hence one less node is created in the virtual DOM.
// ❎
return (
<div>
<Component1 />
<Component2 />
<Component3 />
</div>
);
// ✅
return (
<>
<Component1 />
<Component2 />
<Component3 />
</>
);
9. Don’t Define a Function Inside Render:
Don’t define a function inside render. Move any JS code out of JSX if that doesn’t serve any purpose of rendering or UI functionality. Try to keep the logic inside render to an absolute minimum.
// ❎
return (
<button
onClick={(event) => {
console.log(event.target, "clicked!"); // Avoid this
}}
>
Submit
</button>
);
// ✅
const submitData = (event) => {
console.log(event.target, "clicked!");
};
return <button onClick={submitData}>Submit</button>;
10. Reusing Expressions with Variables:
Declare variables in the block scope code for reusability. As soon as you see the need to use a condition or expression more than once, extract it as a variable instead of repeating the expressions over and over.
// ❎
function getUserPreferences(user) {
if (user.basicDetails.role === 'ADMIN') {
// ...
} else if (user.basicDetails.role === 'OTHER') {
// ...
}
}
// ✅
function getUserPreferences(user) {
const { role } = user.basicDetails;
if (role === "ADMIN") {
// ...
} else if (role === "OTHER") {
// ...
}
}
11. String Props Don’t Need Curly Braces
when being passed to a child component.
// ❎
<NavBar title={"My App"} />
// ✅
<NavBar title="My App" />
12. Lists and the Key prop:
The “key” prop is a special string attribute you need to include when rendering lists of elements. React uses keys to uniquely identify items in an array. With keys, React can pinpoint which item has been changed, added, or removed from the array.
Most of the time when rendering arrays, we might tend to use the index as the key. While this sometimes works, using the index as a key can introduce issues especially if the list is expected to change. Consider this list -
const arr = ["item1", "item2", "item3", "item4", "item5"];
// ❎
{arr.map((elem, index) => <li key={index}>{elem}</li>)}
Currently, the first list item, “item1” is at index zero, but if you added another item at the beginning of the list, the “item1” index would change to 1 which changes the behaviour of your array. Hence make sure to use unique values as keys.
// ✅
{arr.map((elem, index) => <li key={elem}>{elem}</li>)}
13. Arrow functions & implicit return:
Use the JavaScript feature of implicit return in arrow functions to write beautiful code. Let’s say your function does a simple calculation and returns the result.
// ❎
function add(a, b) { return a + b; }
// ✅
const add = (a, b) => a + b;
Similarly while using the arrow function in JSX-
// ❎
{users.map((user) => { return <div key={user.id}>{user.name}</div>; })}
// ✅
{users.map((user) => <div key={user.id}>{user.name}</div>}
14. Import Ordering:
Always try to import things in the following recommended order. The rule of thumb is to keep the import order like this:
● Built-in
● External
● Internal
It improves code readability. Most editors like VSCode provide an option to automatically order the imports on file save.
// ❎
import React from 'react';
import ErrorImg from '../../assets/images/error.png';
import styled from 'styled-components/native';
import colors from '../../styles/colors';
import { PropTypes } from 'prop-types';
// ✅
import React from 'react';
import { PropTypes } from 'prop-types';
import styled from 'styled-components/native';
import ErrorImg from '../../assets/images/error.png';
import colors from '../../styles/colors';
15. File & Folder Structure:
One of the most commonly followed files & folder structures is as follows:-
└── /src
├── /assets — Contains static assets — Icon Svgs, banner images etc.
├── /components — reusable components like navigation, buttons, forms
├── /services — JavaSript modules
├── /utils — utilities, helpers, constants.
├── /views/pages — The majority of the app pages would be here
├── index.js
└── App.js
In turn, each component folder should have its respective files grouped under the same. This maintains a consistent hierarchy throughout the codebase. Eg. For the footer component, the necessary files could include- src/components/footer/index.tsx
, src/components/footer/styles.scss
, src/components/footer/stories.tsx
, src/components/footer/footer.test.tsx
, src/components/footer/types.ts
, etc.
16. Naming Conventions:
Always use PascalCase for components and camelCase for instances & prop names.
// ❎
import formHelper from './formHelper';
const form_Helper = new formHelper();
<MyComponent
UserName="hello"
phone_number={12345678}
/>
// ✅
import FormHelper from './FormHelper';
const formHelper = new FormHelper();
<MyComponent
userName="hello"
phoneNumber={12345678}
/>
17. Naming Rationales:
Boolean variables, or functions that return a boolean value, should start with “is,” “has” or “should”.
// ❎
const done = tasks.length === 0;
// ✅
const isComplete = tasks.length === 0;
Likewise, functions should be named for what they do, not how they do it. In other words, don’t expose details of the implementation in the name. Why? Because how you do it may change someday, and you shouldn’t need to refactor your consuming code because of it. For example, you may load your config from a REST API today, but you may decide to bake it into JavaScript tomorrow.
Also, avoid underscores in function names. (_doSomething()
)
// ❎
const getConfigFromServer = () => …
// ✅
const loadConfig = () => …
18. Reserved Prop Name Usage:
Don’t use DOM component prop names for passing props between components because others might not expect these names.
// ❎
<MyComponent style="dark" />
<MyComponent className="dark" />
// ✅
<MyComponent theme="dark" />
19. Usage of Quotes:
Use double quotes for JSX attributes and single quotes for all other JS.
// ❎
<Foo bar='bar' />
<Foo features={{errors: 'none' }} />
// ✅
<Foo bar="bar" />
<Foo features={{ errors: 'none' }} />
20. JSX in Parentheses:
If the JSX that is being returned by your component or iterator spans more than one line, always wrap it in parentheses.
// ❎
return <MyComponent variant="long"> <MyChild /> </MyComponent>;
// ✅
return (
<MyComponent variant="long">
<MyChild />
</MyComponent>
);
21. Self-Closing Tags:
If your component doesn’t have any children, then use self-closing tags. It improves readability.
// ❎
<Header showMenu></Header>
// ✅
<Header showMenu />
22. Sanitize HTML to prevent XSS Attacks:
Maybe you’ve found yourself in a scenario where you have to use the property dangerouslySetInnerHTML on an element in React, say when having to render the result coming from a rich text editor. Basically, it’s React’s equivalent to Javascript’s innerHTML.
const Markup = () => {
const htmlString = "<p>This is set via dangerouslySetInnerHTML</p>";
return <div dangerouslySetInnerHTML={{ __html: htmlString }} />;
};
The term dangerously is chosen with intention. Using this property can open you up to a cross-site scripting (XSS) attack. So it’s highly recommended that the code that gets set is sanitized first. You could either use a custom-written utility to filter out risky code snippets or use external libraries to help with this.
DOMPurify
is one good example of this.
23. Using Spread Operator to pass Props:
If you have an object, and you want to pass all its keys in JSX, you can use ...
a “spread” syntax to pass the whole object. It helps with avoiding unnecessary object property access & repetition.
// ❎
<ProductCard name={item.name} price={item.price} />
// ✅
<ProductCard {...item} />
24. Using Optional Chaining:
Increases readability and simplifies otherwise long logical conjunctions and complex-looking conditions.
// ❎
const showPublishers = books && books.publishers && books.publishers.length > 0;
// ✅
const showPublishers = books?.publishers?.length > 0;
25. Shorthand Logic Evaluation:
Below are some examples of cases that return truthy/falsy values that can be relatively shortened.
// ❎
length > 0
name !== "" && name !== undefined && name !== null
If using such conditions in javascript code blocks like function, they can be written as:
// ✅
if(length) …
if(name) …
If used inside JSX, they can be first converted to Boolean using the double negation (!!) operator as:
// ✅
{!!length && …}
{!!name && …}
26. Img Alt prop:
Always include an alt prop in your <img>
tags. And, we don’t need to use pictures
or images
in alt
property because the screen readers already announce img
elements as images.
// ❎
<img src="boat.jpg" />
<img src="boat.jpg" alt="Picture of a person rowing a boat" />
// ✅
<img src="hello.jpg" alt="Me waving hello" />
27. Default Props:
It is considered a good practice to set a default value for otherwise optional props.
// ❎
const NavBar = ({ showNotifications }) => ...
// ✅
const NavBar = ({ showNotifications = false }) => ...
// OR
const NavBar = ({ showNotifications }) => {};
NavBar.defaultProps = {
showNotifications: false,
};
28. Inline styles:
It is not considered good practice to use inline styles in React components. Always make use of options like stylesheets, CSS modules, styled components, etc.
// ❎
<div style={{ display: 'none' }}>text</div>
// ✅
<div className="hide">text</div>
29. Async/await over Callbacks:
Promises & callbacks have been the pillar pattern for working with asynchronous code. However, the introduction of async-await has helped achieve the same functionality, in addition to providing better readability and overcoming certain limitations like issues with nested callbacks (callback hells), etc.
// ❎
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
// ✅
async function getUsers() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
console.log(users);
} catch (error) {
console.error(error);
}
}
30. Use Error Boundaries:
As much as we try to make our application robust, errors are still bound to occur. We create user-friendly UIs to handle such error cases. To guard our app against crashing entirely, React provides the feature of Error boundaries through which we can display a fallback UI should an unexpected error occur. You can make use of React’s ErrorBoundary implementation as shown below or use a package like react-error-boundary
.
// ❎
const App = () => {
return <Component>...</Component>;
};
// ✅
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
logError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Oops, something went wrong.</h1>;
}
return this.props.children;
}
}
const App = () => {
return (
<ErrorBoundary>
<Component>...</Component>
</ErrorBoundary>
);
};
31. Functional Components over Class Components:
With the introduction of hooks, there’s practically nothing that you can’t do in Functional components as opposed to class components. And functional components are better because:
● They are much easier to read and test because they are plain JavaScript functions.
● You end up with less code. Also, the resulting translated version is comparatively less.
● It gets easier to separate container and presentational components because you need to think more about your component’s state if you don’t have access to setState() in your component.
● The React team recommends them stating they provide a performance boost.
// ❎
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// ✅
function Welcome({ name }) {
return <h1>Hello, {name}</h1>;
}
32. Use memoization techniques:
Understanding the render cycle of a component can help us in leveraging the various memoization options provided by React to improve the performance of your application and avoid unnecessary re-rendering (especially when dealing with heavy or logically expensive components & computations). Some memoization techniques include:
● React.PureComponent (for class components)
● memo()
● useCallback()
● useMemo()
However, we can not go about memoizing every component in our application since even memoization uses memory and can be less performant when used incorrectly. Therefore, the only caveat is knowing whether any such memoization technique is needed or not and when to use it.
Below is an example of a case when using memo
is beneficial.
// ❎
export const App = () => {
const [userName, setUserName] = useState("John");
const [count, setCount] = useState(0);
const increment = () => setCount((count) => count + 1);
return (
<>
<ChildComponent userName={userName} />
<button onClick={increment}> Increment </button>
</>
);
};
const ChildComponent = ({ userName }) => {
console.log("rendered", userName);
return <div>{userName}</div>;
};
Although the ChildComponent
should render only once because the value of “count” has nothing to do with the ChildComponent
. But, it renders each time you click on the button. A better way would be to change ChildComponent
to this:
// ✅
const ChildComponent = React.memo(({ userName }) => {
console.log("rendered");
return <div> {userName}</div>;
});