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.
Refactoring becomes easier.
Easier to identify and understand the dependencies of a module.
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={true} />
// ✅ 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.
Part 3: Component Structure - Building Reusable and Maintainable Components in React!
Sathish Kumar N ・ May 22 '23
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!😊👩💻👨💻
Top comments (19)
Instead of use prop types use typescript with FC
Thank you for your suggestion!
If you are using JavaScript (without TypeScript), you can continue using
prop-types
to validate the props in your components.If you are using TypeScript, you can benefit from the additional type-checking capabilities it offers by using the
FC
type to define your components' props.Does not have any reason to not use TS.
But you don't need FC.
FC is a unnecessary import, you can destructure the props as the examples below
There's a small mistake in no. 10: Shorthand for boolean props, should be multiSelect={true} replaced by multiSelect prop instead of multiSelect={false}.
Thank you for bringing the issue!
I updated the snippet now.
I think you're mistaken between named exports and named functions. If you write your components like this:
Your editor has no trouble finding out what you mean when you try to use
MyNamedFunctionComponent
.The same goes for variables like this:
What you DONT want to do is:
And infact recommended react eslint rules will warn you not to do that.
I'm also quite unsure if your tree shaking claim is true... If it is true it would only be for webpack 3 maybe. I'm definitely not seeing "use default exports" on this checklist: smashingmagazine.com/2021/05/tree-...
I apologize for any confusion caused in my blog.
I am familiar with the concept of named and default exports. Please refer to the following code snippet to understand the difference between default and named exports:
In the case of default exports, refactoring can be more challenging. Here's an example:
Default export:
However, when using named exports, this confusion can be avoided.
Regarding the claim about tree shaking, it's important to note that tree shaking itself is not directly dependent on the use of default exports.
By utilizing named exports, you gain more control over the exported values. This allows unused values to be eliminated during the bundling process, resulting in a smaller bundle size.
Named exports are objects though. If you have one file, with several named exports, in the background, it just looks like a big object to the bundler and all that code will be included, even if you only use one of the named exports in your code. That is exactly why some libraries recommend you use their default exports and NOT their named exports, because the named exports are all exported from a single
index.ts
file.If you really want to be religious about optimizing for tree shaking, it doesnt matter what kind of export you use, named or default. The only rule is: export one thing per file. In reality though, it doesnt matter that much. If you have some utils file with a bunch of named exports, it doesnt matter that they all get included in the bundle, because the overhead of some small functions is minimal.
I agree your point!
In Tree shaking, the key is to import only the specific exports you need, whether they are named or default exports.
When we using default exports, we don't need to specify a specific name when importing that value into another file. You can give it any name you want when you import it.
You are ChatGPT :P
I didn't use ChatGPT for my comments!
Are you using chat gpt for your replies haha? Same tone of voice..
Ha ha! I am using Quillbot to convert my sentence and improve it further.
I wonder, why do you write all your exports at the bottom?
export
next to each exported declaration.Excellents points. I agree with all of them.
About How you "Avoid huge component" I believe what you do with your components is just as bad if not worse tho. The modularity is too extreme and at the end, it's a nightmare to work with.
Hello,
I thoroughly enjoyed reading your React Best Practices in 2023 series. I'm a developer from South Korea, and I'm interested in translating the four articles included in your React Best Practices in 2023 series into Korean. I would like to post them on my personal blog with proper attribution and links. Would that be possible? I believe it would make it more convenient for Korean developers to access your content. Looking forward to your response :) Have a great day!
React.memo is less performant than just rendering the component again. Under the hood, the memo function has more operations than rendering a simple component
Some comments have been hidden by the post's author - find out more