How to Optimize Redux Toolkit in React
Optimizing Redux Toolkit in React applications is crucial for maintaining high performance and smooth user experiences as your state grows. This article provides a direct, actionable guide on how to enhance Redux Toolkit efficiency, focusing on minimizing unnecessary re-renders, using memoized selectors, leveraging RTK Query for data fetching, and optimizing state structure.
1. Use Granular
Selectors with useSelector
One of the most common performance bottlenecks in React-Redux is unnecessary component re-renders. This happens when a component selects more state than it actually needs.
Instead of selecting an entire state object:
// Avoid: Re-renders when any property of the user changes
const user = useSelector((state) => state.auth.user);Select only the specific, primitive values required by the component:
// Prefer: Only re-renders if the username changes
const username = useSelector((state) => state.auth.user.username);If you must return a new object or array from a selector, use the
shallowEqual comparison function from
react-redux as the second argument to prevent constant
re-renders:
import { useSelector, shallowEqual } from 'react-redux';
const userPermissions = useSelector(
(state) => state.auth.user.permissions,
shallowEqual
);2. Implement
Memoized Selectors with createSelector
When performing expensive computations, filtering, or mapping over
Redux state, use createSelector (which is re-exported by
Redux Toolkit from the Reselect library). This memoizes the output,
meaning the selector will only recalculate when its input state actually
changes.
import { createSelector } from '@reduxjs/toolkit';
const selectItems = (state) => state.cart.items;
// This selector will only recalculate when selectItems changes
export const selectTotalPrice = createSelector(
[selectItems],
(items) => items.reduce((total, item) => total + item.price, 0)
);3. Normalize State
Using createEntityAdapter
Deeply nested or relational data structures can lead to complex
update logic and poor rendering performance. Redux Toolkit’s
createEntityAdapter solves this by normalizing your state
into an ID-based lookup map (similar to a database table).
Normalizing state reduces lookup time to \(O(1)\) and simplifies updates:
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
const usersAdapter = createEntityAdapter();
const usersSlice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState(),
reducers: {
userAdded: usersAdapter.addOne,
userUpdated: usersAdapter.updateOne,
},
});Using createEntityAdapter also automatically generates
pre-optimized selectors like selectAll and
selectById.
4. Leverage RTK Query for API Integration
Manually managing asynchronous data fetching, loading states, and caching in Redux requires a lot of boilerplate and can easily lead to suboptimal performance.
RTK Query (included with Redux Toolkit) automates this process by providing: * Out-of-the-box caching: Prevents duplicate network requests for the same data. * Automatic garbage collection: Removes unused cache data from the Redux store after a configurable time limit. * Optimistic updates: Instantly updates the UI while waiting for server confirmation, creating a faster perceived user experience.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
endpoints: (builder) => ({
getProducts: builder.query({
query: () => 'products',
}),
}),
});5. Keep Redux State Serializable and Minimal
Ensure that you only store plain, serializable JavaScript objects, arrays, and primitives in your Redux store. Avoid storing non-serializable items like class instances, functions, Promises, or DOM nodes.
Keeping state minimal and serializable keeps the Redux DevTools fast and prevents performance overhead in the Redux serialization middleware. If you need derived data, compute it on the fly using memoized selectors rather than saving it to the store.