How to choose the Redux state shape for an app with list/detail views and pagination?

Imagine I have a number of entries(say, users) in my database. I also have two routes, one for list, other for detail(where you can edit the entry). Now I'm struggling with how to approach the data structure.

I'm thinking of two approaches and a kinda combination of both.

Shared data set

  • I navigate to /list, all of my users are downloaded from api a stored in redux store, under the key users, I also add some sort of users_offset and users_limit to render only part of the of the list
  • I then navigate to /detail/<id>, and store currently_selected_user with <id> as the val... which means I will be able to get my user's data with something like this users.find(res => res.id === currently_selected_user)
  • updating will be nice and easy as well, since I'm working with just one data set and detail pointing to it
  • adding a new user also easy, again just working with the same list of users

Now the problem I have with this approach is that, when the list of users gets huge(say millions), it might take a while to download. And also, when I navigate directly to /detail/<id>, I won't yet have all of my users downloaded, so to get data for just the one I need, I'm gonna have to first download the whole thing. Millions of users just to edit one.

Separated data set

  • I navigate to /list, and instead of downloading all of my users from api, I only download a couple of them, depending on what my users_per_page and users_current_page would be set to, and I'd probably store the data as users_currently_visible
  • I then navigate to /detail/<id>, store currently_selected_user with <id> as the val...and instead of searching through users_currently_visible I simply download user's data from api..
  • on update, I'm not gonna update users_currently_visible in any way
  • nor will I on add

What I see as possible problem here is that I'm gonna have to, upon visiting /list, download data from api again, because it might not be in sync with what's in the database, I also might be unnecessarily downloading users data in detail, because they might be incidentally already inside my users_currently_visible

some sort of frankenstein-y shenanigans

  • I detail, I do the same as in Separated data set but instead of directly downloading user's data from api, I first check:
    • do I have any users_currently_visible
    • if so, is there a user with my id between them? if both are true, I then use it as my user data, otherwise I make the api call
  • same happens on update, I check if my user exists between users_currently_visible if so, I also update that list, if not I do nothing

This would probably work, but doesn't really feel like it's the proper way. I would also probably still need to download fresh list of users_currently_visible upon visiting /list, because I might have added a new one..

Is there any fan favorite way of doing this?... I'm sure every single one redux user must have encountered the same things.

Thanks!

Answers:

Answer

Please consult “real world” example from Redux repo.
It shows the solution to exactly this problem.

Your state shape should look like this:

{
  entities: {
    users: {
      1: { id: 1, name: 'Dan' },
      42: { id: 42, name: 'Mary' }
    }
  },
  visibleUsers: {
    ids: [1, 42],
    isFetching: false,
    offset: 0
  }
}

Note I’m storing entities (ID -> Object maps) and visibleUsers (description of currently visible users with pagination state and IDs) separately.

This seems similar to your “Shared data set” approach. However I don’t think the drawbacks you list are real problems inherent to this approach. Let’s take a look at them.

Now the problem I have with this approach is that when then list of users gets huge(say millions), it might take a while to download

You don’t need to download all of them! Merging all downloaded entities to entities doesn’t mean you should query all of them. The entities should contain all entities that have been downloaded so far—not all entities in the world. Instead, you’d only download those you’re currently showing according to the pagination information.

when I navigate directly to /detail/, I wouldn't yet have all of my users downloaded, so to get data for just the one, I'm gonna have to download them all. Millions of users just to edit one.

No, you’d request just one of them. The response action would fire, and reducer responsible for entities would merge this single entity into the existing state. Just because state.entities.users may contain more than one user doesn’t mean you need to download all of them. Think of entities as of a cache that doesn’t have to be filled.


Finally, I will direct you again to the “real world” example from Redux repo. It shows exactly how to write a reducer for pagination information and entity cache, and how to normalize JSON in your API responses with normalizr so that it’s easy for reducers to extract information from server actions in a uniform way.

Answer

I also used normalizr approach before to normalize entities, but the problem with it is that this requires manual work.

If you know Apollo in GraphQL world, you probably know that it supports automatic normalisation, so data for a given object is not stored in multiple places. Thx to that they also support automatic updates, if your server responds with an object with the same id but with updated attrs, Apollo will recognize it and update this object in multiple places.

However, why this luxury should be reserved only for GraphQL? Due to this reason I implemented redux-requests library, which supports automatic normalisation for any API, REST, GraphQL, Firebase, whatever. How does it work? Imagine you have a book list and detailed endpoints. To communicate with them, you would just dispatch Redux actions like that:

const fetchBooks = () => ({
  type: FETCH_BOOKS,
  request: { url: '/books' },
  meta: { normalize: true },
});

const fetchBook = id => ({
  type: FETCH_BOOK,
  request: { url: `/books/${id}` },
  meta: { normalize: true },
})

Now, to update title of a book in both places, we would just do:

const updateBookTitle = (id, newTitle) => ({
  type: UPDATE_BOOK_TITLE,
  request: { url: `books/${id}`, method: 'PATCH', data: { newTitle } },
  meta: { normalize: true },
})

If you are interested with such an approach, more about it could be read here

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.