Published at: December 15, 2021
React continues to evolve, and with every new release, developers face new challenges and mistakes to avoid. In 2021, as React features like Concurrent Mode, React Server Components, and Suspense gained more traction, developers encountered performance bottlenecks, struggled with state management, and misused hooks in ways that impacted their apps’ efficiency.
In this post, we’ll look at some common mistakes React developers made in 2021 and provide solutions to fix them.
1. Overusing State in Functional Components
One of the most frequent mistakes developers make is overusing useState
for everything. While state is essential for interactive UIs, adding too many state variables can lead to unnecessary re-renders, impacting performance.
Why It’s a Problem:
Every time a state variable changes, React re-renders the component. This can cause performance issues if you have too many state updates, especially in large components or complex UIs.
How to Fix It:
- Group Related State Variables: Instead of managing multiple individual pieces of state, consider using a single
useState
hook with an object or array to group related data. - Use
useReducer
for Complex State: For more complex state logic (like when you have multiple actions affecting state), consider usinguseReducer
. It provides better control over updates and prevents unnecessary re-renders.
// Instead of multiple useState calls:
const [name, setName] = useState('');
const [age, setAge] = useState(0);
// Use a single useState for related data:
const [user, setUser] = useState({ name: '', age: 0 });
// Or use useReducer:
const initialState = { name: '', age: 0 };
function userReducer(state, action) {
switch (action.type) {
case 'SET_NAME':
return { ...state, name: action.payload };
case 'SET_AGE':
return { ...state, age: action.payload };
default:
return state;
}
}
const [state, dispatch] = useReducer(userReducer, initialState);
2. Improper Usage of useEffect
Another frequent mistake is misusing the useEffect
hook, leading to unnecessary re-renders and potential performance issues. Developers often forget to specify proper dependencies, causing the effect to run more often than needed.
Why It’s a Problem:
If you don’t specify the dependencies array correctly, useEffect
will run on every render, even if the effect doesn’t need to be triggered. This can significantly slow down your app and lead to performance issues.
How to Fix It:
- Use the Dependency Array Properly: Always specify the dependencies that should trigger the effect. If you want the effect to run only once (on mount), pass an empty array
[]
. - Optimize useEffect: Make sure that the dependencies in the array only include values that truly affect the side-effect. Avoid adding values that don’t need to be there.
useEffect(() => {
console.log('This runs only once after component mount');
}, []); // Empty dependency array ensures it runs only once
useEffect(() => {
console.log('This runs whenever count or name changes');
}, [count, name]); // Only run when count or name changes
3. Not Memoizing Expensive Functions with useCallback and useMemo
React’s re-rendering mechanism is optimized, but when you have expensive calculations or functions that are passed down as props, they may cause unnecessary re-renders. Developers often forget to memoize these functions using useMemo
or useCallback
, causing performance degradation.
Why It’s a Problem:
Without memoization, React treats functions and objects as new instances every time the component re-renders, which can result in unnecessary computations and re-renders of child components.
How to Fix It:
- Use
useCallback
for Functions: If you’re passing functions as props to child components, wrap them inuseCallback
to memoize them. - Use
useMemo
for Expensive Calculations: If you have expensive calculations that only need to be recomputed when certain values change, useuseMemo
to cache the result.
// Memoizing function with useCallback
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
// Memoizing an expensive calculation with useMemo
const expensiveCalculation = useMemo(() => calculateExpensiveValue(data), [data]);
4. Improper or Excessive Use of Context API
While the Context API is powerful for passing down props through deeply nested components, it is often overused or used incorrectly. Developers sometimes try to manage too much state using context, which can lead to unnecessary re-renders across the entire component tree.
Why It’s a Problem:
When context values change, all components that consume the context will re-render, even if they don’t need to. This can lead to unnecessary performance bottlenecks if not managed properly.
How to Fix It:
- Avoid Using Context for Frequently Changing Data: Use context only for state that doesn’t change too often, such as theme settings, user authentication, or language preferences.
- Use Memoization: If your context provides a large object or function, memoize the value to prevent unnecessary re-renders.
const value = useMemo(() => ({ name: 'John', age: 30 }), []); // Memoize context value
5. Over-Rendering Components with Inline Functions and Objects
It’s tempting to define functions or objects directly inside JSX, but this can lead to unnecessary re-renders because React sees them as new instances on every render.
Why It’s a Problem:
When you define a function or object inline, React treats it as a new object every time the component renders. This causes unnecessary re-renders in child components.
How to Fix It:
- Define functions and objects outside JSX: Instead of defining them inside JSX, define them outside the return statement or memoize them with
useMemo
oruseCallback
.
// Bad practice: Inline function
<MyComponent onClick={() => handleClick()} />
// Better: Use useCallback to memoize function
const memoizedClickHandler = useCallback(() => handleClick(), [handleClick]);
<MyComponent onClick={memoizedClickHandler} />
6. Not Handling Errors Properly in Functional Components
React has error boundaries that are typically used with class components, but with functional components, error handling can be overlooked, especially when using hooks.
Why It’s a Problem:
Without proper error handling, bugs and issues in React components can go unnoticed, leading to poor user experience or even app crashes.
How to Fix It:
- Use Error Boundaries: Although error boundaries can’t be used with functional components directly, you can use a wrapper component to catch errors and display fallback UI.
- Gracefully Handle Errors: Use
try...catch
blocks in async functions and properly handle API call errors.
const ErrorBoundary = ({ children }) => {
const [hasError, setHasError] = useState(false);
const handleError = () => setHasError(true);
return hasError ? <div>Error occurred!</div> : children;
};
function MyComponent() {
useEffect(() => {
try {
// Simulate an error
throw new Error('Something went wrong!');
} catch (error) {
console.error(error);
}
}, []);
return <div>My Component</div>;
}
Conclusion
In 2021, React developers made many strides, but with new tools and patterns come new challenges. By avoiding these common mistakes and adopting best practices, you can make your React apps more efficient, scalable, and easier to maintain.
Remember to focus on optimizing state management, properly using hooks, and reducing unnecessary re-renders. By doing so, you’ll avoid the performance pitfalls that can slow down your applications.