Redux-toolkit: Using entity adapters inside reducers in 1.5.0

Created on 10 Dec 2020  路  5Comments  路  Source: reduxjs/redux-toolkit

Hi, would be thankful for a little help here. From the changelog of 1.5.0 I understood that we who use our entity adapters inside reducers are covered by:

We've also updated createEntityAdapter's getSelectors API to use these draft-safe selectors.

So lets define an adapter:

const tabsAdapter = createEntityAdapter<Tab>();
export const tabsSelectors = tabsAdapter.getSelectors<State>((state) => state.tabs);
export const { selectById: selectTabById } = tabsSelectors;

Then use it in some reducer:

    setSomething(state, action: PayloadAction<{ tabId: string; foo: string }>) {
      const { tabId, foo } = action.payload;
      const tab = selectTabById(state, tabId);
      if (tab) {
        tab.foo = foo;
      }
    },

Works in 1.4.0, but fails in 1.5.0 during runtime with:

TypeError: Cannot assign to read only property 'foo' of object '#<Object>'

or this one if the property was not yet set

TypeError: Cannot add property foo, object is not extensible

Could someone please explain what is the migration path here from 1.4.0 -> 1.5.0?

All 5 comments

These are selectors and will not return draft states.
Selectors are meant to return data for you to read. You should not try to modify their return value.

In your case, you'd either use state.entities[tabId], or - and that's what you have entityAdapter for: adapter.updateOne(state, { id: tabId, changes: { foo } })

@phryneas I see, so I handled updates the unsafe way from the very beginning. O:) Thanks a lot for the clarification and super fast response! The update*/upsert* methods are what I needed!

@phryneas Hi, I am deeply sorry for necromancing this thread, but I'd like to have a closely related question: What is actually the best way to use entity adapters for complex and nested update logic inside reducers in 1.5.0? E.g. in 1.4.0 we may have some complex logic like this:

    updateSomething(state, action: PayloadAction<{ tabId: string; foo: any }>) {
      const { tabId, foo } = action.payload;
      const tab = tabAdapter.getSelectors().selectById(state.tabs, tabId);
      if (foo.a) {
        tab.x.a = foo.a;
      }
      if (foo.b.c) {
        tab.someList.splice(foo.b.c, 1);
     }
     // ...etc
    },

I see two options:
1) Rewrite this using tabAdapter.updateOne into some probably-hard-to-read merge of original tab object and the set of wanted changes.
2) Don't use entityAdapter's selectors at all, and just get hold of the tab with const tab = state.tabs[tabId];.
3) Any other and better one..?

Thanks a lot for an advice.

My first observation is that even if you did want to use selectors, you wouldn't want to call getSelectors() every time the reducer runs.

Next, yeah, I don't see a point in trying to abstract that item lookup inside of a reducer. We _know_ that the item is stored as state.entities[id] - it's easy enough to look it up that way.

@markerikson Correct, not calling getSelectors() everytime, that was just an example, in reality we are using stored and correctly typed selector variants (because our entity adapter has an union type). Thanks for the reply, will get rid of the these selectors inside reducers and hopefully finally fully migrate to 1.5.0. :D

Was this page helpful?
0 / 5 - 0 ratings