5 Best Practices to Improve Your React Code Quality
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 applications. Even frameworks such as Next.js use 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.
1 Props Destructuring and Prop Types
Rather than using props and then using props.variableName, it is better to destructure the props to get the properties. It helps other developers to know which properties are available in the code. Destructuring props helps in writing cleaner and more readable code.
Tip: 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.
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.
const MyComponent = ({ list }) => {
const sortedList = useMemo(() => {
return list.sort();
}, [list]);
return (
<div>
{sortedList.map(item => <div key={item}>{item}</div>)}
</div>
);
};
useMemo
This is used to memorize an expensive (resource-heavy) computed value that doesn't need to be recalculated when re-rendering. It is recalculated only if any dependencies required to calculate the value change. This will save resources and improve the performance of the application.
useCallback
This hook is similar to useMemo but for callback functions. It memorizes the callback function and 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 hooks.
Here is an example that shows fetching data using an API. You can create a custom hook for fetching data to 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 a regular function:
- State and Effect Management: You can use React logic and hooks like
useStateanduseEffectfor better state management and cleaner code. - Sharing Logic: Custom hooks provide an elegant way to share logic between components without resorting to render props or higher-order components.
4 Using Error Boundary and Suspense
Error Boundaries help in catching JavaScript errors in the component. Based on the error, they render a fallback UI instead of crashing the entire application. This improves the user experience.
Error Boundary Example
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;
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
const MyComponent = () => {
return <div>MyComponent content</div>;
};
const App = () => {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
};
export default App;
Suspense Example
Suspense lets you "suspend" a component while waiting for an asynchronous operation. It can be used for fetching data or code splitting. It renders a fallback UI while waiting for content to load.
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
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
5 Immutable Data Patterns
Immutable data patterns ensure that state is not modified directly. Instead, a new array or object is returned to represent the new state. This makes state changes predictable and easier to track.
React's re-render optimization techniques, like PureComponent and React.memo, rely on shallow comparison to detect changes. Immutable states make these comparisons efficient.
Example: Updating State in Array and Object
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));
};
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;
});
};
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 involving 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 fundamental in building resilient, performant, and scalable applications — reflecting the ethos of writing truly high-quality code in React.
Comments
Post a Comment