Optimizing Performance in React and React Native: Understanding useEffect, useMemo, and useCallback
Exploring useEffect, useMemo, and useCallback in Greater Depth
Note: A lot of what is described below can also be applied to code written for React projects.
Background
Lately, I have been spending time going through practice problems that primarily focus on displaying data in a FlatList component. Along the way, I became curious about optimization strategies, whether that pertained to the data array passed into a FlatList component, or other props that get passed in.
My usual habit was to filter data within a useEffect hook or to move any renderItem methods outside of the main component definition. However, I soon noticed solutions that made use of useMemo and useCallback hooks, which led me to delve further into these hooks and evaluate when they should be used outside of my original use cases. Here’s what I learned.
useEffect: Handling Side Effects After Render
useEffect is one of the more commonly used hooks in React and React Native (alongside useState). It is used to handle side effects in components. A side effect refers to any operation that affects the outside world, like API requests, or changes that need to be made to the UI, and more. The hook runs after the component renders, enabling you to handle side effects without blocking rendering. When used to update state or interact with external systems, useEffect ensures those operations are executed only after the current render has completed.
When to Use useEffect:
Fetching data from an API or database: to perform asynchronous data-fetching operations after the component renders.
Updating UI elements directly: For example, focusing an input field or manipulating elements via refs.
Setting up subscriptions, event listeners, or timers: Handle cleanup (if necessary) when the component unmounts or when dependencies change.
import { useState, useEffect } from 'react';
import { FlatList } from 'react-native'
export default function DataList() {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
};
fetchData();
}, []); // Empty dependency array means this runs once after the first render
return <FlatList data={data} ... />
}
When Not to Use useEffect:
For logic that doesn’t need side-effects: For example, if you’re simply handling local state or performing calculations based on existing props, it’s better to handle this directly in the render or state change logic.
Redundant state updates: If you’re just setting state based on existing state within the same render cycle, it may be better to manage that directly.
useMemo
useMemo is used to memoize the result of a calculation, ensuring that the value is only recomputed when specific dependencies change. This is helpful when you are performing expensive calculations that don’t need to be repeated on every render.
When to Use useMemo:
Heavy computations: When you have operations like filtering, sorting, or expensive mathematical calculations that don’t need to be run on every render.
Avoid unnecessary recomputations: Memoizing values ensures that unnecessary recalculations are avoided if the data doesn’t change.
import { useState, useEffect, useMemo } from 'react';
import { FlatList } from 'react-native'
export default function DataList() {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
};
fetchData();
}, []); // Empty dependency array means this runs once after the first render
const filteredData = useMemo(() => {
return data.filter(item => {
// Some complex filter code
});
}, [data]); // Only recompute if 'data' or 'filter' changes
return <FlatList data={filteredData} ... />
}
When Not to Use useMemo:
For simple or low-cost computations: If your calculations are not particularly expensive, introducing useMemo could add unnecessary complexity without any performance benefit. In cases like string concatenation or small lists, the natural re-render behaviour is typically sufficient.
When computation happens within a single render: If you’re performing a computation that has no significant performance implication, there’s no need to memoize.
useCallback
useCallback is similar to useMemo, but specifically designed for memoizing functions. It’s useful when a component re-renders frequently due to props changing (because of unoptimized parent re-renders). useCallback ensures that a function is only recreated when its dependencies change, preventing these unnecessary renders and optimizing the rendering process.
When to Use useCallback:
To prevent unnecessary re-renders in child components: When you have a function that’s passed down as a prop and only needs to be recreated when certain dependencies change. This avoids unnecessary re-renders of the child component due to a new function reference being created.
To optimize performance for components with expensive renders: Memoizing the callback function can help prevent unnecessary function recreation during re-renders, which can otherwise impact performance in components with heavy render operations.
import { useState, useEffect, useMemo } from 'react';
import { FlatList, View, Text } from 'react-native'
export default function DataList() {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
};
fetchData();
}, []); // Empty dependency array means this runs once after the first render
const filteredData = useMemo(() => {
return data.filter(item => {
// Some complex filter code
});
}, [data]); // Only recompute if 'data' or 'filter' changes
const renderItem = useCallback(({ item }) => {
return <View><Text>{item.name}</Text></View>
}, [])
return <FlatList data={filteredData} renderItem={renderItem} ... />
}
When Not to Use useCallback:
When no performance issue is present: If passing functions to children doesn’t result in performance issues, or if the children aren’t particularly expensive to render, there’s no need to memoize the function. Overuse of useCallback can introduce unnecessary complexity without improving performance.
In a local context: If a function doesn’t need to be passed around or memoized (e.g., inside the component where it’s not passed to children), useCallback won’t be beneficial.
Conclusion
Knowing when to use or avoid using useEffect, useMemo, and useCallback is essential for writing efficient and maintainable React and React Native components.
Here are the key principles:
useEffect is for side-effects. Avoid it for things that can be handled synchronously within the component.
useMemo prevents unnecessary recalculations of expensive computations. Avoid it for cheap, simple computations.
useCallback prevents unnecessary recreation of functions during renders. Avoid it unless you have performance issues related to unnecessary re-renders due to functions being passed down.