-
-
Save markerikson/ad319fd7b04bd4eecdcfe7bf51dce7b1 to your computer and use it in GitHub Desktop.
// 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 : it's this method:
- https://redux-toolkit.js.org/api/createEntityAdapter#selector-functions
- https://redux.js.org/tutorials/essentials/part-6-performance-normalization#managing-normalized-state-with-createentityadapter
and yes, every adapter instance has .getSelectors()
available.
Adding TS types:
// Line 19
const fetchRooms = createAsyncThunk<ChatRoomEntry[]>(
...
// Line 24
const fetchMessages = createAsyncThunk<Message[], string>(
...
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 🤷♂️
@jeremyisatrecharm that may be related to the proxy issues described with createDraftSafeSelector
here?
@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,
});
@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.
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!
Hmm. That's a good point, actually, I'd forgotten we do that, and thus there shouldn't be any update problems here 🤷♂️
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.
Could you elaborate on .getSelectors? Is it possible to use the method for messagesAdapter?