What are Redux selectors?
Key takeaways:
Redux is a library that provides a predictable state container for JavaScript applications.
Selectors are functions that extract and derive data from the Redux store.
The selectors provide functionality for data extraction, encapsulation, and reusability.
Memoized selectors cache results to prevent unnecessary recalculations and improve performance.
We can use selectors with hooks like
useSelectorto access state within React components efficiently.
When it comes to state management in JavaScript, Redux stands as a popular and powerful library.
It provides a predictable state container for JavaScript applications, making it easier to manage and track changes to the state. However, accessing and deriving data from the Redux store can become increasingly complex as the application grows.
This is where Selectors come into play.
What are selectors?
Selectors are functions that take the Redux state as input and return some derived state or data. They provide an abstraction layer between the state and the components, making our code cleaner and more maintainable. Selectors can be used to:
Extract specific pieces of state: Imagine we have an application with a complex state that includes user information, settings, and other data. A selector can help by extracting just the user information you need for a profile component, ignoring the rest.
Derive data from the state: In an e-commerce application, we might need to show a list of items that are on sale. A selector can derive this data by filtering the full product list, providing only the items with a discount.
Memoize derived data to avoid unnecessary re-renders: Consider a task management app where we need to display the list of completed tasks. By memoizing this derived data, the app can avoid recalculating the completed tasks list every time the state changes, improving performance and preventing unnecessary updates to the UI.
Why should we use selectors?
Let’s see why we use selectors:
Encapsulation: Selectors encapsulate the logic for accessing the state. This makes it easier to change the state shape without impacting the components that consume the state.
Reusability: Once defined, selectors can be reused across different components.
Performance optimization: Memoized selectors can improve the performance of the application by preventing unnecessary re-renders.
Testability: Selectors can be independently tested, which ensures that the data extraction logic is correct.
Benefits of using selectors
The following are the benefits of Selectors:
Simplified state access: Components can access the state without worrying about its structure.
Separation of concerns: Keeps state access logic separate from component logic.
Improved performance: Memoized selectors prevent unnecessary computations and re-renders.
Enhanced maintainability: Easier to manage and refactor state access logic.
Selectors creation
Let’s start with a basic example to illustrate how to create and use selectors in a Redux application.
Step 1: Setting up Redux
First, let’s set up a simple Redux store.
import { createStore } from 'redux';// Initial stateconst initialState = {users: [{ id: 1, name: 'Dexter', age: 25 },{ id: 2, name: 'Sara', age: 30 },],};// Reducer functionfunction userReducer(state = initialState, action) {switch (action.type) {default:return state;}}// Create Redux storeconst store = createStore(userReducer);export default store;
Let’s see the working of the above code:
Line 1: Import the
createStorefunction from thereduxlibrary to create a Redux store.Lines 4–9: Define the
initialStateobject:Line 4: Initialize a
usersarray within the state.Lines 5–8: Populate the
usersarray with two user objects, each containing anid,name, andageproperty.
Line 12: Define the
userReducerfunction withstateandactionparameters, setting the default state toinitialState.Line 13: Use a
switchstatement to handle different action types.Line 14: Add a
defaultcase in theswitchstatement to return the current state unchanged if the action type doesn’t match any cases.Line 15: Return the current state as the default case of the
switchstatement.Line 20: Create the Redux store using
createStoreand pass theuserReducerto it.Line 22: Export the
storeas the default export to make it available for import in other parts of the application.
Step 2: Defining basic selectors
Next, we define some basic selectors to access the state.
export const getUsers = (state) => state.users;export const getUserById = (state, userId) => state.users.find(user => user.id === userId);
In the above code snippet:
Line 1: The
getUsersfunction is defined and exported. It takes thestateobject as an argument and returns theusersarray from the state. This function acts as a selector to retrieve all users from the Redux state.Line 3: The
getUserByIdfunction is defined and exported. It takes thestateobject and auserIdas arguments. The function uses thefindmethod on theusersarray in the state to locate and return the user whoseidmatches the provideduserId. If no matching user is found, it returnsundefined. This function acts as a selector to retrieve a specific user by theirIDfrom the Redux state.
Step 3: Selectors in components
Now, let’s use these selectors in a React component.
import React from 'react';import { useSelector } from 'react-redux';import { getUsers, getUserById } from './selectors';const UserList = () => {const users = useSelector(getUsers);return (<ul>{users.map(user => (<li key={user.id}>{user.name} - {user.age} years old</li>))}</ul>);};export default UserList;
Here’s the breakdown of the above code:
Line 2: Import the
useSelectorhook from thereact-reduxlibrary, which is used to extract data from the Redux store.Line 3: Import the
getUsersselector from the../selectors/selectorsfile, which will be used to select the list of users from the Redux store.Lines 5–6: Define a functional component named
UserListand use theuseSelectorhook to access the Redux store and select the list of users using thegetUsersselector. The selected users are stored in theusersconstant.Lines 9–13: Iterate over the
usersarray using themapmethod. For each user, render anlielement with akeyattribute set to the user’sidto ensure each list item is uniquely identified and displays the user's name and age within thelielement.
Step 4: Memoized selectors
For larger and more complex applications, using memoized selectors can significantly improve performance. The reselect library is commonly used for this purpose.
// memoizedSelectors.jsimport { createSelector } from 'reselect';const selectUsers = (state) => state.users;export const getUsers = createSelector([selectUsers],(users) => users);export const getUserById = createSelector([selectUsers, (state, userId) => userId],(users, userId) => users.find(user => user.id === userId));
Let’s see the breakdown of the above code:
Line 2: Import the
createSelectorfunction from thereselectlibrary. Thereselectis a library for creating memoized selectors, which helps in optimizing performance by avoiding unnecessary recalculations.Line 4: Define a basic selector function named
selectUsers. This function takes the Redux state as an argument and returns theusersarray from the state.Lines 6–9: Define and export a memoized selector named
getUsersusingcreateSelector.Line 7:
createSelectortakes an array of input selectors as its first argument. In this case, it is[selectUsers], which is a single input selector that retrieves theusersarray from the state.Line 8: The second argument is a transformation function that receives the results of the input selectors as arguments. Here, it simply returns the
usersarray.
The
getUsersselector is now memoized, meaning it will only recompute theusersarray if theusersarray in the state changes.
Lines 11–14: Define and export a memoized selector named
getUserByIdusingcreateSelector.createSelectortakes an array of input selectors as its first argument. Here, it has two input selectors:Line 12:
selectUsers, which retrieves theusersarray from the state, and an inline selector(state, userId) => userId, which simply returns theuserIdargument.Line 13: The second argument is a transformation function that receives the results of the input selectors (
usersanduserId) as arguments. It uses thefindmethod on theusersarray to locate and return the user whoseidmatches the provideduserId.
The
getUserByIdselector is now memoized, meaning it will only recompute the result if theusersarray or theuserIdargument changes.
Step 5: Memoized selectors in components
Update the component to use memoized selectors.
import React from 'react';import { useSelector } from 'react-redux';import { getUserById } from './memoizedSelectors';const UserDetails = ({ userId }) => {const user = useSelector(state => getUserById(state, userId));if (!user) {return <div>User not found</div>;}return (<div><h2>{user.name}</h2><p>Age: {user.age}</p></div>);};export default UserDetails;
Here’s the breakdown of the above code:
Line 3: Import the
getUserByIdselector from the../selectors/memoizedSelectorsfile, which will be used to select a specific user from the Redux store by their ID.Line 5: Define a functional component
UserDetailswhich takes auserIdprop.Line 6: Use the
useSelectorhook to access the Redux store and select the user with the specifieduserIdusing thegetUserByIdselector. The selected user is stored in theuserconstant.Lines 8-10: Check if the
useris not found (i.e.,userisnullorundefined). If the user is not found, return adivelement displaying “User not found.”Lines 12-17: If the
useris found, return adivelement containing:An
h2element displaying the user’s name.A
pelement displaying the user’s age.
Before proceeding and seeing all the above codes working together, let's have a brief overview of what we are trying to achieve.
In this example, we’re building a simple React and Redux application to manage user data.
The application consists of a list of users and detailed information about a selected user. The goal is to demonstrate how to use Redux selectors, including memoized selectors, with the reselect library to efficiently manage and access the state.
Let's see this in action!
Code explanation
We have already discussed most of the files already let’s jump to those that we have not observed earlier.
index.js file
Line 2: Import the
createRootfunction fromreact-dom/clientto use the new React 18 rendering API.Line 3: Import the
Providercomponent fromreact-reduxto make the Redux store available to the entire app.Line 4: Import the Redux store from the specified path.
Line 5: Import the main
Appcomponent.Line 7: Create a root to render the app using
createRoot.Lines 9–12: Wrap the
Appcomponent withProviderto pass the Redux store to the entire app and render the mainAppcomponent.
App.js file
Line 6: Use a JSX
divelement as the container for the component’s content.Line 7: Include an
h1element with the text “User Management” to serve as the main heading.Line 8: Render the
UserListcomponent, which will display a list of users.Line 9: Render the
UserDetailscomponent, passing auserIdprop with the value1to display details of the user withid1.
Now, it’s time to understand how selector played its role:
Encapsulating state access logic
Selectors: The
selectors.jsfile contains simple selector functions (getUsersandgetUserById) that encapsulate the logic for accessing specific parts of the state. This encapsulation ensures that components do not directly access the state, promoting better separation of concerns and making the state access logic reusable and easier to maintain.
Leveraging memoization
Memoized selectors: The
memoizedSelectors.jsfile uses thereselectlibrary to create memoized selectors. These selectors are more efficient because they remember the results of the previous computations and return the cached result if the input state has not changed. This reduces unnecessary recomputations and re-renders, enhancing the performance of the application.createSelector: This function fromreselectcreates memoized selectors. For example,getUserByIdmemoizes the result based on the state and theuserId, ensuring that the expensive computation (finding a user by ID) is only performed when necessary.
Conclusion
Redux selectors are an essential tool for any Redux-based application.
They provide a clean and efficient way to access and derive state, leading to more maintainable and performant code. Selectors can significantly enhance the application’s architecture by encapsulating state access logic and leveraging memoization.
Free Resources