Understanding Redux Toolkit's createEntityAdapter: A Comprehensive Overview
Written on
Chapter 1: Introduction to createEntityAdapter
The official Redux Toolkit documentation on createEntityAdapter serves as a valuable guide, yet it often assumes users understand the underlying reasons for its necessity. This can be a challenge for many, and while the documentation briefly mentions the "why" in the Usage Guide, it quickly transitions into practical how-to examples. Therefore, let’s take a moment to explore why utilizing this utility can help standardize our approach to managing arrays of items.
Arrays in State
The documentation emphasizes the importance of createEntityAdapter in the context of API responses, which is quite pertinent. It’s common for an API to return an array of numerous records, often requiring us to manage this array as a complete set or to target specific items by their ID.
In my previous post, Honey, Where's My Array Item?, I discussed various methods for locating an item within an array. While I touched on the Big O notation of each approach, I may not have elaborated on its significance. To put it succinctly, minimizing the number of iterations through a collection enhances performance. When items are stored in an Object, we can access them directly via their properties, eliminating the need for looping altogether.
To illustrate this point, I created a jsPerf example comparing the efficiency of storing entities in an object by ID versus using different methods to iterate through an Array for entity retrieval.
The results clearly indicate that utilizing Object access is significantly faster. Thus, it makes logical sense to standardize our approach. If we frequently access entities by a specific property, it’s practical to store them in an object indexed by that property from the outset, which is precisely what createEntityAdapter achieves. It constructs a data structure featuring an Array of IDs (allowing us to track which entities we have and to maintain default ordering) alongside an Object that holds the entities, indexed by their IDs.
With this structure, we can easily retrieve an entity using entityState.entities[someId]. However, we might wonder about the usage of IDs and whether the adapter.upsertMany(adapter.getInitialState(), raw) function is overly complex for data storage. Indeed, it may seem that way, but this merely scratches the surface of what createEntityAdapter offers.
Customizing createEntityAdapter
createEntityAdapter accepts an optional parameters argument, which can include two properties: selectId and sortComparer. The selectId property should always be a function that identifies the property of the object to be regarded as the entity’s ID. However, things become particularly interesting when a sortComparer is provided. In this case, createEntityAdapter generates a different type of adapter that maintains the order of the IDs based on the comparison results.
It’s crucial to understand that the adapter and the data are distinct entities. Ignoring TypeScript nuances, if you had multiple collections using the same ID property and sorting criteria, a single adapter could serve all of them. Conversely, if you have an Array that requires a different sorting method and index property, you can run it through various adapters to yield entirely different EntityStates.
This leads us to the necessity of using upsertMany to populate our data from an Array. The adapter we created includes numerous CRUD operations designed to manage entities while preserving the sort order (if a sortComparer was supplied), and upsertMany is one such operation. Since the data exists independently from the adapter, it’s essential to provide initial data to "modify" with the CRUD functions. This is why we utilize adapter.getInitialState() when first populating our data — it will return {ids:[], entities:{}}. If data already exists, that would be used instead.
I put "modify" in quotation marks because, in Redux, we don’t actually alter anything. Instead, we return a new EntityState reflecting the requested changes applied to the original state.
In summary, employing createEntityAdapter allows us to create an object equipped with methods for transforming a raw array into an object for rapid access, along with an array of IDs that facilitates sorted data retrieval.
Accessing Entities
This brings us to the process of retrieving data from the EntityState that the adapter generates. You might expect the adapter to provide methods for accessing data similar to those for managing it, but that’s not the case.
Instead, the adapter features a getSelectors method. While this may seem excessive for many scenarios, the rationale behind it is straightforward. In Redux, you might not provide the EntityState directly to the selector. Instead, you could be passing the entire Redux state, and the selector needs to pinpoint the correct EntityState.
Consider a situation where you have both books and categories in your Redux store, both utilizing the same ID property and sorting criteria. You could retrieve the set of selectors for books with adapter.getSelectors((state) => state.books) and for categories with adapter.getSelectors((state) => state.categories). However, if you’re already in a context dealing with books, you could utilize the selectors returned from adapter.getSelectors() directly.
Once you have the selectors, you can feed one of them the data (and an argument, when necessary) to extract the desired information from the EntityState structure. For instance, you could add the following code to our CodeSandbox:
const { selectTotal } = adapter.getSelectors();
console.log(selectTotal(entityState)); // Outputs: 4
Here, we don’t require an argument for getSelectors because we created our EntityState directly and have it readily available — no need to search for it within Redux.
TypeScript Considerations
When I initially utilized createEntityAdapter in the context of RTK Query, I found it immensely helpful to specify the type of data that would be stored in that context. However, I must admit, it wasn’t immediately clear to me how to define the data type for an endpoint that transformed an Array into an EntityState using transformResponse. Let’s clarify that here with data similar to our previous examples.
Where to Go from Here
If you found this exploration valuable, you may appreciate additional resources on this topic. For those wishing to support dedicated authors like myself, consider subscribing through my link.