Skip to content

Instantly share code, notes, and snippets.

@markerikson
Last active March 27, 2024 06:37
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save markerikson/ad319fd7b04bd4eecdcfe7bf51dce7b1 to your computer and use it in GitHub Desktop.
Save markerikson/ad319fd7b04bd4eecdcfe7bf51dce7b1 to your computer and use it in GitHub Desktop.
Nested `createEntityAdapter` example
// Example of using multiple / nested `createEntityAdapter` calls within a single Redux Toolkit slice
interface Message {
id: string;
roomId: string;
text: string;
timestamp: string;
username: string;
}
interface ChatRoomEntry {
id: string;
messages: EntityState<Message>;
}
const roomsAdapter = createEntityAdapter<ChatRoomEntry>();
const messagesAdapter = createEntityAdapter<Message>();
const fetchRooms = createAsyncThunk(
"chats/fetchRooms",
chatsAPI.fetchRooms
);
const fetchMessages = createAsyncThunk(
"chats/fetchMessages",
async (roomId) => {
return chatsAPI.fetchMessages(roomId);
}
)
const chatSlice = createSlice({
name: "chats",
initialState: roomsAdapter.getInitialState(),
reducers: {
},
extraReducers: builder => {
builder.addCase(fetchRooms.fulfilled, (state, action) => {
const roomEntries = action.payload.map(room => {
return {id: room.id, messages: messagesAdapter.getInitialState()};
});
roomsAdapter.setAll(state, roomEntries);
})
.addCase(fetchMessages.fulfilled, (state, action) => {
const roomId = action.meta.arg;
const roomEntry = state.entities[roomId];
if (roomEntry) {
messagesAdapter.setAll(roomEntry.messages, action.payload);
}
})
}
})
/*
Resulting state:
{
ids: ["chatRoom1"],
entities: {
chatRoom1: {
id: "chatRoom1",
messages: {
ids: ["message1", "message2"],
entities: {
message1: {id: "message1", text: "hello"},
message2: {id: "message2", text: "yo"},
}
}
}
}
}
*/
@voyager5874
Copy link

Could you elaborate on .getSelectors? Is it possible to use the method for messagesAdapter?

@ashuvssut
Copy link

Adding TS types:

// Line 19
const fetchRooms = createAsyncThunk<ChatRoomEntry[]>(
...

// Line 24
const fetchMessages = createAsyncThunk<Message[], string>(
...

@jeremyisatrecharm
Copy link

I ran into an issue where I was using getSelector to update a nested adapter via upsertOne and when I passed in state that I got via the selector, the update no-oped, but when I directly passed instate.entites[id].subEntity it worked 🤷‍♂️

@markerikson
Copy link
Author

@jeremyisatrecharm that may be related to the proxy issues described with createDraftSafeSelector here?

https://redux-toolkit.js.org/api/createSelector

@jeremyisatrecharm
Copy link

@markerikson thanks for the past response and for sharing - I didn't know about that.

I am not sure that's causing it, I am not using createSelector, simply making methods like

class Selectors {
  static getOuterEntity = sliceAdapter.getSelectors().selectById;
  static innerEntityState = (state: InitialSliceState, outerId: string) =>
    this.getOuterEntity(state, outerId)?.innerEntityState;
}

Then inside the reducer, this works:

innerAdapter.upsertOne(state.entities[outerId]?.innerEntityState!, {
        innerId,
        innerValue,
      });

But this doesn't:

innerAdapter.upsertOne(Selectors.innerEntityState(state, outerId), {
        innerId,
        innerValue,
      });

@markerikson
Copy link
Author

@jeremyisatrecharm : yes, that's my point. getSelectors() uses createSelector inside. So, if you're using that in a reducer, there's a good chance that it's seeing the proxy wrapper and not detecting changes correctly.

@jeremyisatrecharm
Copy link

jeremyisatrecharm commented May 24, 2023

Ah okay, I think one thing that confused me is the link you provided notes:

Besides re-exporting createSelector, RTK also exports a wrapped version of createSelector named 
createDraftSafeSelector that allows you to create selectors that can safely be 
used inside of createReducer and createSlice reducers with Immer-powered mutable logic.
...
All selectors created by entityAdapter.getSelectors are "draft safe" selectors by default.

and I am using upsertOne inside reducers; so that made me think I wanted to use those selectors.

I'm all unblocked now - thank you!

@markerikson
Copy link
Author

Hmm. That's a good point, actually, I'd forgotten we do that, and thus there shouldn't be any update problems here 🤷‍♂️

@lamhungypl
Copy link

lamhungypl commented Sep 26, 2023

How do you make a selector for nested properties?
AFAIK, it'd be something like below

export const messagesByIdSelector = createSelector(
  curerntRoomSelector,
  (state: RootState, messageId: EntityId) => messageId,
  (room, messageId) => {
    const items = room?.messages;
    if (!items) {
      return;
    }

    const allMessages = messageEntities
      .getSelectors(() => items)
      .selectAll(() => items);

    return allMessages.find((item) => item.message_id === messageId);
  }
);

I achieved the expected data, but the const allMessages = messageEntitties .getSelectors(() => items) .selectAll(() => items); seems repeated many times.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment