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
useSelector
to 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.
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.
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.
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.
Let’s start with a basic example to illustrate how to create and use selectors in a Redux application.
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 createStore
function from the redux
library to create a Redux store.
Lines 4–9: Define the initialState
object:
Line 4: Initialize a users
array within the state.
Lines 5–8: Populate the users
array with two user objects, each containing an id
, name
, and age
property.
Line 12: Define the userReducer
function with state
and action
parameters, setting the default state to initialState
.
Line 13: Use a switch
statement to handle different action types.
Line 14: Add a default
case in the switch
statement 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 switch
statement.
Line 20: Create the Redux store using createStore
and pass the userReducer
to it.
Line 22: Export the store
as the default export to make it available for import in other parts of the application.
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 getUsers
function is defined and exported. It takes the state
object as an argument and returns the users
array from the state. This function acts as a selector to retrieve all users from the Redux state.
Line 3: The getUserById
function is defined and exported. It takes the state
object and a userId
as arguments. The function uses the find
method on the users
array in the state to locate and return the user whose id
matches the provided userId
. If no matching user is found, it returns undefined
. This function acts as a selector to retrieve a specific user by their ID
from the Redux state.
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 useSelector
hook from the react-redux
library, which is used to extract data from the Redux store.
Line 3: Import the getUsers
selector from the ../selectors/selectors
file, which will be used to select the list of users from the Redux store.
Lines 5–6: Define a functional component named UserList
and use the useSelector
hook to access the Redux store and select the list of users using the getUsers
selector. The selected users are stored in the users
constant.
Lines 9–13: Iterate over the users
array using the map
method. For each user, render an li
element with a key
attribute set to the user’s id
to ensure each list item is uniquely identified and displays the user's name and age within the li
element.
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 createSelector
function from the reselect
library. The reselect
is 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 the users
array from the state.
Lines 6–9: Define and export a memoized selector named getUsers
using createSelector
.
Line 7: createSelector
takes an array of input selectors as its first argument. In this case, it is [selectUsers]
, which is a single input selector that retrieves the users
array 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 users
array.
The
getUsers
selector is now memoized, meaning it will only recompute theusers
array if theusers
array in the state changes.
Lines 11–14: Define and export a memoized selector named getUserById
using createSelector
. createSelector
takes an array of input selectors as its first argument. Here, it has two input selectors:
Line 12: selectUsers
, which retrieves the users
array from the state, and an inline selector (state, userId) => userId
, which simply returns the userId
argument.
Line 13: The second argument is a transformation function that receives the results of the input selectors (users
and userId
) as arguments. It uses the find
method on the users
array to locate and return the user whose id
matches the provided userId
.
The
getUserById
selector is now memoized, meaning it will only recompute the result if theusers
array or theuserId
argument changes.
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 getUserById
selector from the ../selectors/memoizedSelectors
file, which will be used to select a specific user from the Redux store by their ID.
Line 5: Define a functional component UserDetails
which takes a userId
prop.
Line 6: Use the useSelector
hook to access the Redux store and select the user with the specified userId
using the getUserById
selector. The selected user is stored in the user
constant.
Lines 8-10: Check if the user
is not found (i.e., user
is null
or undefined
). If the user is not found, return a div
element displaying “User not found.”
Lines 12-17: If the user
is found, return a div
element containing:
An h2
element displaying the user’s name.
A p
element 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!
We have already discussed most of the files already let’s jump to those that we have not observed earlier.
index.js
fileLine 2: Import the createRoot
function from react-dom/client
to use the new React 18 rendering API.
Line 3: Import the Provider
component from react-redux
to make the Redux store available to the entire app.
Line 4: Import the Redux store from the specified path.
Line 5: Import the main App
component.
Line 7: Create a root to render the app using createRoot
.
Lines 9–12: Wrap the App
component with Provider
to pass the Redux store to the entire app and render the main App
component.
App.js
fileLine 6: Use a JSX div
element as the container for the component’s content.
Line 7: Include an h1
element with the text “User Management” to serve as the main heading.
Line 8: Render the UserList
component, which will display a list of users.
Line 9: Render the UserDetails
component, passing a userId
prop with the value 1
to display details of the user with id
1.
Now, it’s time to understand how selector played its role:
Encapsulating state access logic
Selectors: The selectors.js
file contains simple selector functions (getUsers
and getUserById
) 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.js
file uses the reselect
library 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 from reselect
creates memoized selectors. For example, getUserById
memoizes the result based on the state and the userId
, ensuring that the expensive computation (finding a user by ID) is only performed when necessary.
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