3

The Redux documentation (https://redux.js.org/usage/structuring-reducers/normalizing-state-shape) recommends to avoid nested data and to normalize and flatten it instead. But I wonder if it really is a good idea to flatten the state in the case of nested one-to-many relationships.

Let's say I want to model a collection of books, where each books has a certain number of pages, and each page has a certain number of sentences. Every sentence is in only one page and every page is in only one book. From what I understand, the Redux docs suggest the following normalized flat structure. Note that pages and sentences need ids that are globally unique.

state = {
    books: {
        allIds: ["book1", "book2"],
        byId: {
            "book1": {
                /*name: "First book",*/
                pages: ["page1", "page2"],
            },
            "book2": {/*...*/},
        },
    },
    pages: {
        byId: {
            "page1": {
                /*pageNumber: "1",*/
                sentences: ["sentence1", "sentence2"]
            },
            "page2": {/*...*/},
        }
    },
    sentences: {
        byId: {
            "sentence1": {
                /*contents: "First sentence"*/
            },
            "sentence2": {/*...*/},
        }
    },
};

But in practice I found that this representation makes it very difficult to do certain operations. For instance (deep) cloning a book requires coming up with a whole lot of new unique ids (for the cloned book, all the cloned pages, and all the cloned sentences), but I have no idea where I can generate them: the reducer is not allowed to generate random ids, and the action creator only has access to the book id so it doesn’t know how many random ids need to be generated.

On the other hand, in the representation below where we nest the {allIds, byId} objects, cloning books is very easy: you just need to come up with one new id for the new book and then do a regular deep clone. Note that the ids are now only required to be locally unique.

state.books = {
    allIds: ["book1", "book2"],
    byId: {
        "book1": {
            /*name: "First book",*/
            pages: {
                allIds: ["page1", "page2"],
                byId: {
                    "page1": {
                        /*pageNumber: "1",*/
                        sentences: {
                            allIds: ["sentence1", "sentence2"],
                            byId: {
                                "sentence1": {
                                    /*contents: "First sentence"*/
                                },
                                "sentence2": {/*...*/},
                            }
                        }
                    },
                    "page2": {/*...*/},
                }
            }
        },
        "book2": {/*...*/}
    }
};

This nested representation also seems to avoid every single pitfall mentioned in the Redux docs:

When a piece of data is duplicated in several places, it becomes harder to make sure that it is updated appropriately.

  • Not relevant for one-to-many relationships as data is not duplicated to begin with.

Nested data means that the corresponding reducer logic has to be more nested and therefore more complex. In particular, trying to update a deeply nested field can become very ugly very fast.

  • Well, no, using Immer (included by default in Redux Toolkit) updating a deeply nested field is super easy. Even without Immer, it’s not that complicated. On the other hand, the flat representation makes a lot of other things way more complex (in particular cloning a book which seems incredibly complicated to me, but also adding or removing pages). Yes, in order to access a sentence the sentenceId is not enough, you now also need to give the pageId and the bookId, but that’s just two more arguments to a bunch of function calls.

Since immutable data updates require all ancestors in the state tree to be copied and updated as well, and new object references will cause connected UI components to re-render, an update to a deeply nested data object could force totally unrelated UI components to re-render even if the data they're displaying hasn't actually changed.

  • Doesn’t seem to be an issue either, as long as you don’t select the whole state.books.byId[bookId] object. If you only select state.books.byId[bookId].name and state.books.byId[bookId].pages.allIds, then you have all the information you need, and changing a page or a sentence won’t make the book component rerender. It seems just as efficient as the flat version. I guess there is slightly more boilerplate, especially if you need to select many other fields, but it shouldn’t be too hard to manage.

In summary, I think that turning an array of books/pages/sentences into an {allIds, byId} object and having every book/page/sentence component select its own data is definitely a good idea, but I really don’t see the point of going one step further and flattening out everything. Keeping the data nested seems significantly easier to work with and doesn’t seem to have any real drawbacks.

So I guess my question is:

Am I missing something? Is there something else I would gain by flattening my state, which would make it worth figuring out how to clone a book?

philipxy
  • 14,867
  • 6
  • 39
  • 83
Guillaume Brunerie
  • 4,676
  • 3
  • 24
  • 32
  • The reasons are all the reasons for relational DB 1NF. Notions of ["1NF"](https://stackoverflow.com/a/40640962/3404097) basically all say, collapse a group of tables or columns with names that differ by parameters into one table with a column per parameter. [Is storing a delimited list in a database column really that bad?](https://stackoverflow.com/q/3653462/3404097) – philipxy Oct 14 '21 at 03:41
  • @philipxy You seem to be talking about something slightly different, namely linking each page to its own book, rather than linking each book to its array of pages. That seems even less efficient (at least in the context of Javascript/Redux) and not even the Redux docs go that far (they still have a list of comments stored in every blog post in their example). Or is there any specific argument that you feel would apply in my case as well? – Guillaume Brunerie Oct 14 '21 at 07:18
  • I am talking about exactly what the documentation is talking about, namely the benefits of the relational model & relational DB normalization. The relational model transformed DB technology & there are tons of presentations including published textbooks online free re normalization & also when one might want to denormalize. So I commented so you could research the topic & associated arguments. Relational relations/tables are not "linked" for recording or querying. "Link" is a non-relational term ("relationship" is also misused for this) for foreign key constraint, for integrity & optimization. – philipxy Oct 14 '21 at 22:30
  • But the documentation doesn't even normalize the state to 1NF (it keeps lists of foreign keys in one of the columns), it only flattens it. I must admit I don't know much about databases, but I read everything I could find about both dealing with a complex state in Redux and database normalization in general (including your two links) and still couldn't find any argument that would apply here. [...] – Guillaume Brunerie Oct 15 '21 at 05:07
  • [...] I'm simply working on a large Redux app with a complex state that needs to be queried and updated many times a second, so I decided to follow best practices and flatten it. But not only it made the logic way more complicated to write, it also introduced a severe performance hit (I had to disable Immer in some of the reducers, even though it's working fine in the nested version), and it also seemed much harder to guarantee consistency and integrity (the nested structure guarantees that every page belongs to exactly one book, by construction). [...] – Guillaume Brunerie Oct 15 '21 at 05:08
  • [...] Anyway, I don't really have a problem, I reverted my app to the nested state structure and now everything works much better, the code is easy to understand, and performance is very good, so I'm not planning to go back to the flat version unless someone can come up with a very good concrete argument. – Guillaume Brunerie Oct 15 '21 at 05:08

0 Answers0