2

I have an array of messages returned from the server where each chat message has the following format:

export interface ChatMessage {
  _id: string | number;
  text: string;
  createdAt: Date | number;
  user: ChatUser;
  groupId: number;
}

interface ChatUser {
  _id: string | number;
  name: string;
}

I want to normalize the list of messages by groupId AND then by message Id nested inside to achieve something like this:
const messages = {
  ids: ['group1', 'group2'],
  entities: {
   group1: {
      ids: ['msg1', 'msg2'],
      msg1: {},
      msg2: {},
    },
   group2: {
      ids: ['msg3', 'msg4'],
      msg3: {},
      msg4: {},
   },
};

How can I achieve that with createEntityAdapter or with the normalizr library?

abdou-tech
  • 687
  • 1
  • 8
  • 16

1 Answers1

1

I think the heavy lifting can be done with selectors if the state is well designed. I'd try something like this:

const initialState = {
    messages: {}, // messages by id

    // map groupIds to an object with messageIds and their createdAt timestamp
    groups: {
        /* Example:
        groupId1: { messageId1: '2022-05-23', messageId2: '2022-05-22'}
        */
    },
    users: {}, // users by id
};

chatMessagesFromServer.forEach(message => {
    const { user } = message;

    // Save message by id, but only with a reference to the userId
    const normalizedMessage = { ...message, userId: user._id };
    delete normalizedMessage.user;
    state.messages[message._id] = normalizedMessage;

    // Associate groups with messages
    if (state.groups[message.groupId]) {
        state.groups[message.groupId][message.id] = message.createdAt;
    } else {
        state.groups[message.groupId] = { [message.id]: message.createdAt };
    }

    // Save user by id
    state.users[user._id] = user;
});

Retrieving all messages in one group in chronological order for example is a text book example of a memoized selector accessing state.groups to get the message ids, then enriching that list of ids with the message data from state.messages.

timotgl
  • 2,865
  • 1
  • 9
  • 19
  • Note that the `createEntityAdapter` API in our official Redux Toolkit package is specifically designed to help manage CRUD operations on a normalized lookup table of items: https://redux-toolkit.js.org/api/createEntityAdapter , https://redux.js.org/tutorials/essentials/part-6-performance-normalization#normalizing-data – markerikson May 24 '22 at 03:41
  • @markerikson I couldn't find nested relationships addressed in the docs. Depending on how many of those auto-generated CRUD actions and selectors you need, I'd still say implementing only the parts you need yourself is a valid option. For example in this case, the `group` entity doesn't hold data about itself (besides the ID), so one state for the group/message relationship should be sufficient. The array of IDs can be done with a selector and `Object.keys()`, unless there are some downsides i'm not aware of. – timotgl May 24 '22 at 07:07
  • @timotgl thanks for the answer, but with the way you structured the state how can update a specific message for example when I only have the `groupId` and `messageId` to begin with. In the example I gave, that would be as simple as `messages.entities[groupId][messageId].propertyThatNeedsChange = newValue`. The main reason I want to normalize the list of messages is because I'll have to update each message individually many times according to some websocket events. – abdou-tech May 24 '22 at 08:41
  • @markerikson Yes but I have read pretty much everything I could find in the docs and it seems that `createEntityAdapter` handles only first level normalization and nesting cannot be done with it. Plus as @timotgl has mentioned, in my case, groupId isn't an entity, it just holds the id value that's why I couldn't seem to figure out how to do it even with the normalize library. – abdou-tech May 24 '22 at 08:49
  • @abdou-tech You can access the individual message via `state.messages[messageId]` and modify any property like you wrote. I'm assuming message ids are unique across groups. – timotgl May 24 '22 at 12:23
  • 1
    We don't specifically cover nested relationships in the docs, and that's largely because A) `createEntityAdapter` doesn't do any of that work itself, and B) trying to handle those relationships in some way devolves into a "client side database", which is going to be fairly app-specific and outside of RTK's "most common use cases" scope. That said, I recently showed an approach for "nested adapters" for a chat room / messages scenario in this answer: https://stackoverflow.com/a/72253798/62937 – markerikson May 24 '22 at 23:43