Introduction
Writing code is easy but writing optimized, clean, and readable code takes more than applying logic. It takes some experience with the technology along with learning from other code and developers. One of the quotes from the Clean Code book by Robert Cecil Martin says
It is not enough for the code to work
-Robert C. Martin
The quote emphasizes writing optimized code along with the code that works.
React has been the most popular framework for building frameworks. Even there is a framework such as NextJS that uses React under the hood to build a better framework. So, knowing how to write better code in React can help in writing better code in many other frameworks.
So, today we are going to look into some code examples that show best practices that you can use to improve the JavaScript code quality in React.
Let’s get started.
1. Props Destructuring and Prop Types
Rather than using props
and then using props.variableName
, it is better to restructure the props to get the properties. It helps other developers to know which properties are available in the code. Destructing props helps in writing cleaner and readable code.
Along with destructuring the props, you should also validate the types of props using ‘prop-types’. You can set up the project in TypeScript for adding type-safety to React. TypeScript is popular for providing type safety to the application. If you do not want to use TypeScript for your whole project then use the ‘prop-types’ library to add type-safety to your props.
Here is the code example demonstrating props destructuring and types safety:
import React, { useState, useEffect } from 'react';
function MyComponent({ initialCount }) {
const [count, setCount] = useState(initialCount);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
2. Optimizing Performance with useMemo and useCallback Hook
useMemo
and useCallback
are the two hooks that can help in optimizing the performance of the application in React.
useMemo
This is used to memorize an expensive(resource-expensive) computed value that doesn’t need to be recalculated when re-rendering. It is recalculated if any dependencies are required to calculate the value changes. This will save resources and improve the performance of the application.
Use Case of useMemo Hook:
const MyComponent = ({ list }) => {
const sortedList = useMemo(() => {
return list.sort();
}, [list]);
return (
<div>
{sortedList.map(item => <div key={item}>{item}</div>)}
</div>
);
};
useCallback
This hook is the same as useMemo but for the callback function. It memorizes the callback function. This is useful when passing callbacks to optimized child components that rely on reference equality(same memory address rather than content) to prevent unnecessary renders.
Use Case of usecallback Hook:
const MyComponent = ({ a, b }) => {
const memoizedCallback = useCallback(() => {
return a + b;
}, [a, b]);
return <ChildComponent onCalculate={memoizedCallback} />;
};
3. Custom Hook
Creating custom hooks in React can help in encapsulating and reusing logic across multiple components. They are specially designed to work within the React component lifecycle and have access to other React features like state and other hooks.
Here is an example that shows the fetching of Data using API. You can create a custom hook for fetching data use across the application.
import { useState, useEffect } from 'react';
function useFetchData(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
This also shows why you can prefer a custom hook over any regular function. Here are those:
-
State and Effect Management: You can use the React logic and hooks like
useState
anduseEffect
for better state management and writing cleaner code. - Sharing Logic: Custom hooks provide an elegant way to share logic between components without resorting to rendering props or higher-order components. You can easily reuse complex logic across your project.
4. Using Error Boundary and Suspense
Use Error Boundary and Suspense together for a robust error handling and loading experience for your application. Let’s look into both of them.
Error Boundary
Error boundary is a React feature that helps in catching JavaScrpt errors in the component. Based on the error, it also renders a fallback UI instead of crashing the entire application. This can be used to manage errors in applications in a better way and also improve the user experience of the application.
Error boundary can be defined in a class and then used in functional components. Here is an example of this:
ErrorBoundary Class
import React from 'react';
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error("Error caught by Error Boundary:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong: {this.state.error.message}</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
Functional Component
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
const MyComponent = () => {
// Component logic here. For demonstration, we'll keep it simple.
return <div>MyComponent content</div>;
};
const App = () => {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
};
export default App;
Suspense
Suspense is also a React feature that lets you “suspend” a component while waiting for some asynchronous operation. It can be used while fetching data or code splitting. It also renders a fallback UI while waiting for the content to load. It combines with React.lazy()
to import the child component.
Here is an example showing the use case of Suspense.
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
Combining Error Boundaries and Suspense
Both components can be combined to provide better error handling and improve the user experience. You can wrap a Suspense component in an error boundary to catch any errors that might occur during loading or in the lazy-loaded components.
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
5. Immutable Data Patterns
It is a method that allows the state to be immutable, you don’t modify the state of an array or object directly but instead return a new array or object that represents the new state. This makes the state changes predictable and transparent. We can also track the changes to the state.
React's re-render optimization techniques, like PureComponent
and React.memo
, rely on shallow comparison to detect changes. An immutable state ensures that changes to the state produce new objects, making these comparisons reliable and efficient.
Here is an example of Updating State in Array.
const [items, setItems] = useState([{ id: 1, name: "Item 1" }]);
// Adding an item (without mutating the original state)
const addItem = newItem => {
setItems([...items, newItem]);
};
// Removing an item (without mutating the original state)
const removeItem = itemId => {
setItems(items.filter(item => item.id !== itemId));
};
We can use this object as per the below example.
const [user, setUser] = useState({ name: "Alice", age: 30 });
// Updating a property (without mutating the original state)
const updateUser = newName => {
setUser({ ...user, name: newName });
};
// Removing a property (without mutating the original state)
const removeProperty = propName => {
setUser(prevUser => {
const { [propName]: _, ...rest } = prevUser;
return rest;
});
};
Connect With Me
Let's connect and stay informed on all things tech, innovation, and beyond! 🚀
Conclusion
Embracing best practices in React goes beyond just writing functional code; it's about creating efficient, readable, and maintainable applications. Techniques like props destructuring enhance clarity while using useMemo
and useCallback
optimizes performance. Custom hooks demonstrate React's power in logic reuse and sharing across components. Additionally, integrating error boundaries and Suspense ensures robust error handling and a smooth user experience, particularly in scenarios of data fetching and component loading.
Adopting immutable data patterns is crucial for maintaining predictable state mutations, aligning with React's optimization strategies for component lifecycle and performance. In essence, these practices are not merely for meeting functional requirements but are fundamental in building resilient, performant, and scalable applications, reflecting the ethos of writing truly high-quality code in React.
I hope this article will help you in writing better code in React. Thanks for reading the article.