Best Practices for Well-Optimized Components in React

Kaloyan Kosev
Dev Labs
Published in
15 min readJan 14, 2023

--

Performance optimization is like fine-tuning a sports car. Just as a mechanic adjusts the engine, suspension, and other systems of a car to maximize its speed and handling, a developer can optimize the performance of a React component by making small adjustments to its code and design.

Performance is a key factor to consider when building applications with React, especially when developing hybrid apps with React Native for mobile devices. These devices are often less powerful than desktop computers or laptops, and poorly optimized components can negatively impact the user experience much more on mobile devices. In my experience, using optimization techniques is critical when developing hybrid apps with React Native to ensure a smooth and responsive user experience.

One reason why some developers may not prioritize optimization is that they often work on high-end devices and may not feel the performance penalties that most users of the app may experience. However, it’s important to keep in mind that the average user of the app may not be using a flagship phone or a high-end device, and optimizing components can help ensure a good user experience for a wider range of devices.

Fortunately, React provides a variety of tools and techniques for optimizing component performance. In this article, we will explore a range of best practices for optimizing the performance of React components. From using memoization to minimize rerenders, to synchronizing DOM manipulation with the browser layout, we will cover a variety of techniques and tools that can help you create well-optimized components for your React apps.

Whether you’re a seasoned developer or new to React, these tips and tricks will help you create responsive and efficient components that provide a seamless experience for your users.

We will also look at some less commonly used optimization techniques, such as the React.useRef hook and the React.useMemo hook. By the end of this article, you should have a good understanding of how to write efficient and performant React components.

Optimizing component rendering

One way to optimize the rendering of functional components is to use the React.memo higher-order component. React.memo is similar to the React.PureComponent class for class-based components, in that it allows you to optimize the performance of a functional component by skipping the rendering of the component if the props have not changed.

To use React.memo, you can wrap your functional component with the React.memo higher-order component, like this:

import React from 'react';

const MyComponent = React.memo(({ name }) => {
// component render logic goes here

return <p>Hi, {name}</p>
});

By default, React.memo performs a shallow comparison of the props to determine if the component should re-render. This means that if the props are referentially different, the component will re-render, but if the props are shallowly equal, the component will not re-render.

However, you can also provide a custom comparison function as a second argument to React.memo to customize the props comparison. This can be useful if your props are complex objects or arrays and you want to use a more thorough comparison than the default shallow comparison.

For example, you can use the _.isEqual method from the lodash library to perform a deep comparison of the props:

import React from 'react';
import _ from 'lodash';

const areEqual = (prevProps, nextProps) => {
return _.isEqual(prevProps, nextProps);
};

const MyComponent = React.memo(props => {
// component render logic goes here
}, areEqual);

Or, you can write your own custom comparison function that only compares certain props, or that uses a different comparison strategy:

import React from 'react';

const areEqual = (prevProps, nextProps) => {
// custom comparison logic goes here
return prevProps.timestamp === nextProps.timestamp;
};

const MyComponent = React.memo(props => {
// component render logic goes here
}, areEqual);

Using React.memo can help you optimize the performance of functional components by avoiding unnecessary re-renders, which can save on computation time and improve the overall performance of your application.

According to best practices, based on what I’ve read and researched, it is generally recommended to wrap all React components you write in React.memo. This is because it's better to optimize everything and some additional memoization won't hurt compared to skipping to add React.memo for a component.

However, there may be rare cases when you want to avoid using React.memo, such as when the custom comparison function is very heavy (runs slowly) and the component often needs to re-render. In this case, the props comparison performed by React.memo could hurt performance rather than help it. However, based on my experience, this situation is rare, and the opposite scenario - missing performance optimization when it is needed - is much more common.

Optimizing component functions

Optimizing the performance of functions is crucial when it comes to writing efficient and well-optimized React components. In this article, we will focus on two specific methods for optimizing function performance — React.useMemo and React.useCallback. These hooks are designed to help you minimize the number of re-renders that occur in your component, which can help you to achieve a significant performance boost.

Memoize expensive calculations with React.useMemo

The React.useMemo hook allows you to optimize the performance of your React components by memoizing the result of a function. This means that the function is only recalculated when one of its dependencies changes. This can be useful for optimizing expensive calculations that do not need to be performed on every render.

To use the React.useMemo hook, you can pass a function and a list of dependencies as arguments. The hook will return the result of the function, but it will only recalculate the result if one of the dependencies changes.

Here is an example of using the React.useMemo hook to memoize the result of an expensive calculation:

import { useMemo } from 'react';

const MyComponent = (props) => {
// Calculate the result of an expensive calculation using the _.isEqual method from Lodash
const result = useMemo(() => {
// Perform the expensive calculation here
let sum = 0;
for (let i = 0; i < props.items.length; i++) {
sum += props.items[i].value;
}
return sum;
}, [props.items]);

return (
<div>
The result is {result}.
</div>
);
};

When the component re-renders, the useMemo hook will check if the props.items has changed. If it hasn't, useMemo will return the previous memoized result, thus avoiding recalculating the expensive calculation. If it has, useMemo will recalculate the expensive calculation and return the new result. This can help improve the performance of the component by avoiding unnecessary recalculations.

Memoize event handlers and other functions with React.useCallback

The React.useCallback hook allows you to optimize the performance of your React components by memoizing event handlers. This means that the event handler is only recreated when one of its dependencies changes. This can be useful for optimizing event handlers that do not need to be recreated on every render.

To use the React.useCallback hook, you can pass a function and a list of dependencies as arguments. The hook will return a memoized version of the function, which will only be recreated if one of the dependencies changes.

Here is an example of using the React.useCallback hook to memoize an event handler:

import { useCallback } from 'react';

const MyComponent = (props) => {
// Memoize the event handler using the useCallback hook
const handleClick = useCallback(() => {
// Event handler logic goes here
console.log('Button clicked');
}, []);

return (
<button onClick={handleClick}>
Click me
</button>
);
};

In this example, the useCallback hook is used to memoize the handleClick event handler. The event handler will only be recreated if the dependencies (in this case, an empty array) change. This can help to optimize the performance of the MyComponent component, as the event handler will not be recreated on every render.

The React.useCallback hook can be useful in a variety of situations, including when you pass a function as a prop to a child component or when you attach a function to an event handler like the onClick event of a button.

Here’s an example of how you can use React.useCallback to optimize the rendering of a child component that receives a callback function as a prop:

import React, { useState, useCallback } from 'react';

const ParentComponent = () => {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);

return (
<div>
<ChildComponent onClick={handleClick} />
</div>
);
};

const ChildComponent = React.memo(({ onClick }) => {
return <button onClick={onClick}>Click me</button>;
});

In this example, the ParentComponent renders a ChildComponent and passes an onClick prop to it. The ChildComponent renders a button that calls the onClick callback when clicked.

If the ParentComponent were to re-render for any reason (e.g. due to a change in its own state or props), the ChildComponent would also re-render because its props have changed. However, because the ChildComponent is wrapped in React.memo, it will only re-render if the onClick prop has actually changed.

The React.useCallback hook is used to create the handleClick callback, which is passed to the ChildComponent as the onClick prop. The useCallback hook takes a function and an array of dependencies as arguments. The hook will return a memoized version of the function that only changes if one of the dependencies has changed.

In this case, the handleClick callback only changes if the count state value has changed, so the ChildComponent will only re-render if the count state value has changed. If the ParentComponent re-renders for any other reason, the ChildComponent will not re-render because the handleClick callback has not changed.

Memoize functions that are invoked only within the component

Using the React.useCallback hook to wrap functions that are invoked only within the component can provide a performance benefit if the function is expensive to compute and the component re-renders frequently.

The React.useCallback hook returns a memoized version of the function that only changes if one of the dependencies passed to the hook has changed. This means that if the component re-renders and the dependencies have not changed, the memoized version of the function will be used, which can avoid the overhead of recomputing the function.

Here’s an example of how you can use React.useCallback to optimize a function that is invoked only within the component:

import React, { useState, useCallback } from 'react';

const MyComponent = () => {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);

const computeResult = useCallback(() => {
// perform expensive computation here
return 42;
}, []);

return (
<div>
<button onClick={handleClick}>
You have clicked this button {count} times
</button>
<div>{computeResult()}</div>
</div>
);
};

In this example, the MyComponent component renders a button that increments a counter when clicked and a div that displays the result of an expensive computation performed by the computeResult function.

The computeResult function is wrapped with the React.useCallback hook and is given an empty array of dependencies. This means that the memoized version of the function will never change, and the function will only be recomputed if the component re-renders.

If the component re-renders for any reason other than a change in the count state value, the memoized version of the computeResult function will be used, which can avoid the overhead of recomputing the function.

On the other hand, if the count state value changes and the component re-renders, the computeResult function will be recomputed. In this case, using the React.useCallback hook may not provide any performance benefit.

Whether or not using the React.useCallback hook to optimize functions that are invoked only within the component is worthwhile will depend on the specific circumstances of your application, including how expensive the function is to compute and how frequently the component re-renders.

Optimize render methods

In React, it is important to avoid creating new object or array references in render methods whenever possible. This is because creating new references can trigger unnecessary re-renders of components that depend on those references.

For example, consider the following component:

const MyComponent = (props) => {
const items = props.items.slice();

return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
};

In this example, the MyComponent component creates a new array reference every time it renders by calling the slice method on props.items. This new array reference will be different from the previous reference, even if the contents of the array are the same. As a result, any components that depend on the items array will re-render unnecessarily.

To avoid creating new object or array references in render methods, you can use techniques such as memoization or the React.useMemo and React.useCallback hooks. You can also consider using the React.memo higher-order component to optimize the performance of components that depend on object or array references.

Spreading an object in a React component (for example, by using the ... operator to spread the props object in a child component) should not significantly impact performance. This is because the spread operator creates a shallow copy of the object, rather than a deep copy.

However, it is important to keep in mind that spreading an object in a component can have performance implications if the object is large or if the component re-renders frequently. In these cases, it may be more efficient to pass only the specific props that the component needs, rather than spreading the entire object.

For example, consider the following component:

const MyComponent = (props) => {
return (
<ChildComponent {...props} />
);
};

In this example, the MyComponent component spreads the entire props object into the ChildComponent component. If the props object is large or if the MyComponent component re-renders frequently, this could potentially impact the performance of the ChildComponent component.

To optimize the performance of the ChildComponent component in this case, you could consider passing only the specific props that the component needs, rather than spreading the entire props object. For example:

const MyComponent = (props) => {
return (
<ChildComponent prop1={props.prop1} prop2={props.prop2} />
);
};

This way, the ChildComponent component will only receive the props that it needs, rather than the entire props object. This can help to optimize the performance of the component, especially if the props object is large or if the MyComponent component re-renders frequently.

Other well-known optimization recommendations

Sometimes, it’s the small things that can make a big difference when it comes to minimizing DOM updates in React. Here are a couple of “tiny things” to keep in mind:

  • Use the key prop when rendering a list of items. The key prop is used to uniquely identify each item in a list, and it helps React to optimize the rendering of lists. By providing a stable and unique key for each item, you can ensure that React can efficiently update the list without re-rendering the entire list.
  • Use the React.Fragment component to group elements without adding extra nodes to the DOM. The React.Fragment component allows you to group elements without adding an extra node to the DOM. This can be useful when you need to render a list of elements, but you don't want to add an extra wrapper element to the DOM.
  • The React.useState hook is a powerful tool for managing state in functional components, but it can also lead to unnecessary re-renders if used excessively. To optimize your React components, it's important to use the React.useState hook sparingly, only for values that are directly related to the rendering of the component.

Optimizing DOM manipulation

Optimizing DOM manipulation is an important part of building performant React applications. Manipulating the DOM can be a resource-intensive operation, and it’s important to minimize the number of DOM manipulations as much as possible to ensure good performance.

React.useRef

One way to optimize DOM manipulation in React is to use the React.useRef hook. It allows you to create a mutable reference to a DOM element, which you can update without triggering a re-render. This can be useful for cases where you need to update the position or size of an element, or manipulate it in some other way that does not affect the rendered output of the component.

In this example, we use the React.useRef hook to create a reference to an input element, and then we use the reference to focus the input when a button is clicked. This allows us to manipulate the DOM without triggering a re-render of the component:

import { useRef } from 'react';

const MyInput = () => {
const inputRef = useRef(null);

const handleButtonClick = () => {
inputRef.current.focus();
};

return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleButtonClick}>Focus input</button>
</>
);
};

Memory leaks

Another way to optimize DOM manipulation in React is to use the React.useEffect hook with a cleanup function. React.useEffect allows you to specify a function that runs after the component renders, which can be used to perform DOM manipulation or add event listeners.

It is important to use a cleanup function with React.useEffect to avoid memory leaks. When a component unmounts, any side effects that were created by React.useEffect should be cleaned up to prevent memory leaks. You can do this by returning a cleanup function from the React.useEffect callback.

For example, here is how you can use React.useEffect to add an event listener to a DOM element, and clean up the event listener when the component unmounts:

import { useEffect, useRef } from 'react';

const MyComponent = () => {
const elementRef = useRef(null);

useEffect(() => {
const element = elementRef.current;
const listener = () => { /* event listener logic goes here */ };
element.addEventListener('click', listener);

return () => {
element.removeEventListener('click', listener);
};
}, []);

return <div ref={elementRef}>Click me</div>;
};

This ensures that the event listener is added when the component mounts and removed when the component unmounts, preventing memory leaks.

React.useLayoutEffect

Another way to optimize DOM manipulation in React is to use the React.useLayoutEffect hook. React.useLayoutEffect is similar to React.useEffect, but it runs synchronously after the DOM has been updated. This can be useful for cases where you need to read the layout of an element after it has been updated, or if you need to perform DOM manipulation that affects the layout of the page.

For example, you might use React.useLayoutEffect to read the width and height of an element after it has been updated, and then use this information to adjust the layout of the page.

import { useLayoutEffect, useRef } from 'react';

const MyComponent = () => {
const elementRef = useRef(null);

useLayoutEffect(() => {
const element = elementRef.current;
const { width, height } = element.getBoundingClientRect();
// do something with the width and height
}, []);

return <div ref={elementRef}>I affect the layout</div>;
};

It is important to note that React.useLayoutEffect can block the browser's rendering, so it should be used sparingly. Use React.useEffect instead if the DOM manipulation does not need to be synchronized with the browser layout.

I hope this helps to clarify the role of React.useLayoutEffect in optimizing DOM manipulation in React. I’ve prepared one more example of using React.useLayoutEffect to optimize DOM manipulation in React:

import { useLayoutEffect, useRef } from 'react';

const MyComponent = () => {
const elementRef = useRef(null);

useLayoutEffect(() => {
const element = elementRef.current;
const parent = element.parentNode;
const { width: parentWidth } = parent.getBoundingClientRect();
element.style.width = `${parentWidth}px`;
}, []);

return <div ref={elementRef}>I affect the layout</div>;
};

In this example, we use React.useLayoutEffect to set the width of an element to the width of its parent after the parent has been updated. This ensures that the layout of the page is updated correctly, without causing any visual disruption.

Although these examples are a bit more abstract, I hope they help to illustrate the role of React.useLayoutEffect in optimizing DOM manipulation in React.

Props drilling performance complications

When building a complex component, it is often necessary to pass data down through multiple levels of children. This can lead to a situation known as “props drilling,” where the same props are passed down through multiple components, even if those components do not directly use the props. This can make the component structure more difficult to understand and can also lead to performance issues if the component tree is large.

When data is passed down through multiple levels of children, it can lead to unnecessary re-renders of components that do not actually use the data. For example, consider the following component structure:

const Parent = (props) => (
<Child1 data={props.data}>
<Child2 data={props.data}>
<Child3 data={props.data} />
</Child2>
</Child1>
);

In this example, the Parent component passes the data prop down to three child components: Child1, Child2, and Child3. If the data prop changes, all three child components will be re-rendered, even if they do not actually use the data prop. This can lead to performance issues if the component tree is large, as it can result in many unnecessary re-renders.

One way to avoid props drilling is to use the React.useContext hook. The React.useContext hook allows you to access a context value from within a functional component.

To use the React.useContext hook, you will first need to create a context using the React.createContext function. This function returns a context object that you can use to provide values to components that are rendered within the context.

import { createContext, useContext } from 'react';

// Create a context object
const MyContext = createContext();

const MyComponent = () => {
// Access the context value
const contextValue = useContext(MyContext);

return (
<div>
{contextValue}
</div>
);
};

To provide a value to the context, you can use the MyContext.Provider component. This component accepts a "value" prop that you can use to set the value of the context.

import { createContext, useContext } from 'react';

// Create a context object
const MyContext = createContext();

const MyComponent = () => {
// Access the context value
const contextValue = useContext(MyContext);

return (
<div>
{contextValue}
</div>
);
};

const App = () => (
<MyContext.Provider value="Hello world!">
<MyComponent />
</MyContext.Provider>
);

In this example, the MyComponent component is rendered within the context of the MyContext.Provider component, and it is able to access the value of the context using the useContext hook. This allows you to avoid props drilling and make it easier to manage data flow through your component tree.

Alternatively, you can also consider using a state management library such as Redux or MobX to manage data flow in your application. These libraries provide mechanisms for sharing data between components without the need for props drilling.

Conclusion

In conclusion, optimizing the performance of your React components is crucial for creating a smooth and responsive user experience. There are many techniques and tools available for optimizing the performance of your components, including the React.memo, React.useMemo, React.useCallback, React.useEffect, React.useLayoutEffect, and React.useContext hooks.

By using these techniques to optimize DOM manipulation, minimize state updates, and memoize expensive calculations and event handlers, you can significantly improve the performance of your React components.

Remember to also consider small, but often overlooked techniques such as using the key prop for lists and using the React.Fragment component to avoid unnecessary DOM nodes.

By following these best practices, you can ensure that your React components are well-optimized and provide a seamless experience for your users.

--

--

Front-End Engineer, currently focused on React, React Native, and NodeJS. In the 10k+ reputation club on StackOverflow. Maker. Nano influencer. superkalo.com