Michał
Michał

Reputation: 905

How to structure state for lazy loading items per category in redux

Let's say I have an api endpoint /categories which returns an array of categories

[
  {
    id: '1'
    name: 'category-1'  
  },
  {
    id: '2'
    name: 'category-2'  
  }
  ...
]

and endpoints for retriving the items within category items/:categoryId which returns an array of items

[
  { id: '1', name: 'item-1' },
  { id: '2', name: 'item-2' }
  ...
]

On the UI I display a list of categories, which I can expand and lazy load the list of items. I want to be able to have multiple categories expanded and need to be able to add, edit and delete items.

What is the best way to organize the state for such scenerio?

At the moment my state looks like this:

{
  entities: {
    categories: {
      '1': {
        id: '1'
        name: 'category-1'  
      },
      '2': {
        id: '2'
        name: 'category-2'  
      },
      ...
    },
    items: {
      '1': {
        id: '1'
        name: 'item-1'  
      },
      '2': {
        id: '2'
        name: 'item-2'  
      },
      ...
    }
  },

  categories: {
    ids: ['1', '2', ...],
    isFetching: bool
    error: null
  },
  itemsByCategory: {
    '1': {
      ids: ['1', '2',...]
      isFetching: bool,
      error: null
    }
    ...
  }
} 

In itemsByCategory the keys are ids of categories, if items for the given category are not loaded yet, the key will not exists on itemsByCategory.

This solution works, but has some drawbacks. In order to delete item I have to pass two keys (item id and category id) instead of just item id (I could also go through all categories to find item, but it might become slow).

I am also not happy with checking if items for given category were loaded. (first I have to check if the key with category id is defined on itemsByCategory), so my selectors become a little bit complicated.

Is there any better way to shape the state for such cases?

Upvotes: 9

Views: 814

Answers (2)

Marco Scabbiolo
Marco Scabbiolo

Reputation: 7449

If category is a property of an item every item should have it's categoryId property, to store only a reference to it and avoid replicating the categories everywhere. If the items returned by the API call don't include the category you can assign immediately after you have the results from the API, since you know the category because you've used it to retrieve the items in the first place.

Both categories and itemsByCategory replicate the information you already have in entites by storing the ids of the elements, you don't need to store those.

If the only thing you're lazily loading are the items of every category, in categories create an array of loaded categories to check if you have to make the API call or not.

itemsByCategory looks completely useless to me, unless you're also lazily loading more information about each item.

I wouldn't worry about looping through all of the items to retrieve those that correspond to each category, as long as you only do it for those categories that are expanded. After all React is meant to be used this way and is really good at it. If you're having performance issues it's probably because you're changing the parent component and forcing a re-render of all the lists.

If all of this fails you just have too much data to render, and you might rethink the UI and add some type of pagination. Another option is to purge the items from entities once the category is collapsed, and remove the category from the list of loaded ones, but this might negatively affect your users.

Upvotes: 1

fkulikov
fkulikov

Reputation: 3199

Not much can be done regarding the need to pass category id along with item id in order to remove an item from a category. This comes from the fact that they are connected.

Abstracting away from your particular use case, every time you want to alter connection between two logical entities (establish new one or break existing one) you have to address that connection in some way. It can be done:

  1. Directly: providing ids of both entities.
  2. Indirectly: providing id of only one of them, and getting id of another from some sort of a map.
  3. Hacky: making entities ids derivable one from another by some rules.

Getting back to your use case it means:

  1. The way you don't like: providing category id along with item id.
  2. Either iterating over categories to find which one holds given item, or introducing categoryByItem map in your state, or having categoryId field in each of your items.
  3. Altering items ids to keep appropriate category id inside of them: item.id = item.id + '/' + category.id. Then it's just a matter of splitting item id by '/' to get category's one.

Regarding the issue with checking itemsByCategory for presence of category id keys, why not just populate this object along with populating entities.categories object? Every time new category (with, let's say, id categoryId) is being loaded, create an empty object (or not empty, but with a shape indicating unloaded state) in itemsByCategory under the categoryId key.

The other possibility, especially in case you find yourself making null checks too often, is to use idx helper function from Facebook. With it's help your selector will look like:

const isItemsLoaded = (state, categoryId) => idx(state, _ => _.itemsByCategory[categoryId].isLoaded);

Upvotes: 0

Related Questions