Welcome to Part 4 of our series on “React best practices in 2023“! In this part, we will explore various techniques and strategies to write clean and efficient code in your React applications. By following these best practices, you can improve the maintainability, performance, and readability of your codebase.
Let’s dive in and learn how to write clean and efficient React code that not only works well but is also easier to understand, maintain, and scale.
1. Implement error boundaries to handle component errors gracefully
Wrap your components or specific sections of your application with error boundaries to catch and handle errors in a controlled manner.
This prevents the entire application from crashing and provides a fallback UI or error message, improving the user experience and making it easier to debug issues.
Higher-Order Component (HOC) – withErrorBoundary:
// HOC for error boundary
const withErrorBoundary = (WrappedComponent) => {
return (props) => {
const [hasError, setHasError] = useState(false);
const [errorInfo, setErrorInfo] = useState(null);
useEffect(() => {
const handleComponentError = (error, errorInfo) => {
setHasError(true);
setErrorInfo(errorInfo);
// You can also log the error to an error reporting service here
};
window.addEventListener('error', handleComponentError);
return () => {
window.removeEventListener('error', handleComponentError);
};
}, []);
if (hasError) {
// You can customize the fallback UI or error message here
return <div>Something went wrong. Please try again later.</div>;
}
return <WrappedComponent {...props} />;
};
};
Usage:
// HOC for error boundary
import withErrorBoundary from './withErrorBoundary';
const Todo = () => {
// Component logic and rendering
}
const WrappedComponent = withErrorBoundary(Todo);
2. Use React.memo for functional components
React.memo is a higher-order component that memoizes the result of a functional component, preventing unnecessary re-renders.
By wrapping your functional components with React.memo, you can optimize performance by skipping re-renders when the component’s props have not changed.
Here is an example,
// ❌
const TodoItem = ({text}) => {
return <div> {text} </div>
}
// Todo
const Todo = () => {
//... Component logic
return <div>
//.. Other elements
<TodoItem //.. />
</div>
}
In this example, we have a functional component called TodoItem that receives a name prop and renders a todo text.
By default, the component will re-render whenever Todo parent component re-render.
To optimize performance, we can wrap TodoItem with React.memo, creating a memoized version of the component. This memoized component will only re-render if its props have changed.
// ✅ Memoized version of TodoItem using React.memo
const TodoItem = React.memo(({text}) => {
return <div> {text} </div>
})
By using React.memo, we can prevent unnecessary re-renders and optimize the performance of functional components.
However, it’s important to note that React.memo performs a shallow comparison of props. If your component receives complex data structures as props, ensure that you handle prop updates appropriately for accurate memoization.
3. Use Linting for Code Quality
Utilizing a linter tool, such as ESLint, can greatly improve code quality and consistency in your React projects.
By using a linter, you can:
Ensure consistent code style
Catch errors and problematic patterns
Improve code readability and maintainability
Enforce coding standards and conventions
4. Avoid default export
The problem with default exports is that it can make it harder to understand which components are being imported and used in other files. It also limits the flexibility of imports, as default exports can only have a single default export per file.
// ❌ Avoid default export
const Todo = () => {
// component logic...
};
export default Todo;
Instead, it’s recommended to use named exports in React:
// ✅ Use named export
const Todo = () => {
}
export { Todo };
Using named exports provides better clarity when importing components, making the codebase more organized and easier to navigate.
- Named imports work well with tree shaking.
Tree shaking is a term commonly used within a JavaScript context to describe the removal of dead code. It relies on the import and export statements to detect if code modules are exported and imported for use between JavaScript files.
5. Use object destructuring
When we use direct property access using dot notation for accessing individual properties of an object, will work fine for simple cases.
// ❌ Avoid direct property access using dot notation
const todo = {
id: 1,
name: "Morning Task",
completed: false
}
const id = todo.id;
const name = todo.name;
const completed = todo.completed;
This approach can work fine for simple cases, but it can become difficult and repetitive when dealing with larger objects or when only a subset of properties is needed.
Object destructuring, on the other hand, provides a more concise and elegant way to extract object properties. It allows you to destructure an object in a single line of code and assign multiple properties to variables using a syntax similar to object literal notation.
// ✅ Use object destructuring
const { id, name = "Task", completed } = todo;
It reduces the need for repetitive object property access.
Supports the assignment of default values.
Allows variable renaming and aliasing.
6. Use fragments
Fragments allow for cleaner code by avoiding unnecessary wrapper divs when rendering multiple elements.
// ❌ Avoid unnecessary wrapper div
const Todo = () => (
<div>
<h1>Title</h1>
<ul>
// ...
</ul>
</div>
);
In the above code, Unnecessary wrapper div can add unnecessary complexity to the DOM structure, potentially impacting the accessibility of your web page.
// ✅ Use fragments
const Todo = () => (
<>
<h1>Title</h1>
<ul>
// ...
</ul>
</>
);
7. Prefer passing objects instead of multiple props
when we use multiple arguments or props are used to pass user-related information to component or function, it can be challenging to remember the order and purpose of each argument, especially when the number of arguments grows.
// ❌ Avoid passing multiple arguments
const updateTodo = (id, name, completed) => {
//...
}
// ❌ Avoid passing multiple props
const TodoItem = (id, name, completed) => {
return(
//...
)
}
When the number of arguments increases, it becomes more challenging to maintain and refactor the code. There is an increased chance of making mistakes, such as omitting an argument or providing incorrect values.
// ✅ Use object arguments
const updateTodo = (todo) => {
//...
}
const todo = {
id: 1,
name: "Morning Task",
completed: false
}
updateTodo(todo);
Function becomes more self-descriptive and easier to understand.
Reducing the chances of errors caused by incorrect argument order.
Easy to add or modify properties without changing the function signature.
Simplify the process of debugging or testing functions to passing an object as an argument.
8. Use arrow functions
Arrow functions provide a more concise syntax and lexical scoping, eliminating the need for explicit this
binding and improving code readability.
// ❌
function sum(a, b) {
return a + b;
}
Above code can result in verbose code and potentially lead to misunderstandings regarding the context and binding of this
.
// ✅ Use arrow function
const sum = (a, b) => a + b;
Arrow functions make the code more compact and expressive.
It automatically bind the context, reducing the chances of
this
related bugs.It improves code maintainability.
9. Use enums instead of numbers or strings
// ❌ Avoid Using numbers or strings
switch(status) {
case 1:
return //...
case 2:
return //...
case 3:
return //...
}
Above code that is harder to understand and maintain, as the meaning of numbers may not be immediately clear.
// ✅ Use Enums
const Status = {
NOT_STARTED: 1,
IN_PROGRESS: 2,
COMPLETED: 3
}
const { NOT_STARTED, IN_PROGRESS COMPLETED } = Status;
switch(status) {
case NOT_STARTED:
return //...
case IN_PROGRESS:
return //...
case COMPLETED:
return //...
}
Enums have meaningful and self-descriptive values.
Improve code readability.
Reducing the chances of typos or incorrect values.
Better type checking, editor autocompletion, and documentation.
10. Use shorthand for boolean props
// ❌
<Dropdown multiSelect={false} />
// ✅ Use shorthand
<Dropdown multiSelect />
Shorthand syntax for boolean props improves code readability by reducing unnecessary verbosity and making the intention clear.
11. Avoid using indexes as key props
// ❌ Avoid index as key
const renderItem = (todo, index) => {
const {name} = todo;
return <li key={index}> {name} </>
}
Using indexes as key props can lead to *incorrect rendering * especially when adding, removing, or reordering list items.
It can result in poor performance and incorrect component updates.
// ✅ Using unique and stable identifiers
const renderItem = (todo, index) => {
const {id, name} = todo;
return <li key={id}> {name} </>
}
Efficiently update and reorder components in lists.
Reducing potential rendering issues.
Avoids in-correct component update.
12. Use implicit return in small functions
// ❌ Avoid using explicit returns
const square = value => {
return value * value;
}
When we use explicit return can make small function definitions unnecessarily longer and harder to read.
It may result in more cluttered code due to additional curly braces and explicit return statements.
// ✅ Use implicit return
const square = value => value * value;
Implicit return reduces code verbosity.
Improves code readability.
It can enhance code maintainability by focusing on the main logic rather than return statements.
13. Use PropTypes for type checking
// ❌ Bad Code
const Button = ({ text, enabled }) =>
<button enabled>{text}</button>;
Above code can lead to passing incorrect prop types, which may result in runtime errors or unexpected behavior.
// ✅ Use PropTypes
import PropTypes from 'prop-types';
const Button = ({ enabled, text }) =>
<button enabled> {text} </button>;
Button.propTypes = {
enabled: PropTypes.bool
text: PropTypes.string.isRequired,
};
It helps catch error on compile time.
It provides better understanding and expected type of the component.
PropTypes act as a documentation for other developers working with the component.
14. Prefer using template literals
// ❌ Bad Code
const title = (seriesName) =>
"Welcome to " + seriesName + "!";
Above code can result in verbose code and make string interpolation or concatenation more difficult.
// ✅ Use template literals
const title = (seriesName) =>
`Welcome to ${seriesName}!`;
It simplify string manipulation by allowing variable interpolation within the string.
It makes code more expressive and easier to read.
It support multi-line strings without additional workarounds.
Improving code formatting.
15. Avoid huge component
Avoiding huge components in React is crucial for maintaining clean, modular, and maintainable code.
Large components tend to be more complex, harder to understand, and prone to issues. Let’s explore an example to illustrate this concept:
// ❌ Avoid huge component
const Todo = () => {
// State Management
const [text, setText] = useState("");
const [todos, setTodos] = useState([])
//... More states
// Event Handlers
const onChangeInput = () => //...
// Other event handlers
return (
<div>
<input //.. />
<input //.. />
<button //.. />
<list //... >
<list-item //..>
</list/>
</div>
)
};
export default Todo;
In the above example, we have a component called Todo
, which contains multiple state variables and event handlers and elements.
As the component grows, it becomes harder to manage, debug and understand.
You can check below blog for to address this, it’s recommended to break down such huge components into smaller, reusable, and focused components.
Stay tuned for an upcoming blog dedicated to optimization techniques in React. We’ll explore additional strategies to enhance performance and efficiency in your components. Don’t miss out on these valuable insights!
Happy coding!😊👩💻👨💻