Search code examples
javascriptreduxredux-orm

How to get a one-to-many field as an array in redux-orm


I have the following models for a chat application using redux-orm. Each Conversation contains many Messages, but one message can only belong to a single Conversation:

export class Message extends Model {
  static modelName = 'Message';
  static fields = {
    id: attr(),
    text: attr(),
    created: attr(),
    from: fk('User'),
    conversation: fk('Conversation', 'messages')
  };
}

export class Conversation extends Model {
  static modelName = 'Conversation';
  static fields =  {
    id: attr(),
    created: attr(),
  };
}

I'm using the following selector to get a list of conversations with their respective messages.

export const getConversations = createSelector(
  getOrm,
  createOrmSelector(orm, session =>  {
    return session.Conversation
      .all()
      .toModelArray()
  })
);

The problem? The messages property of each Conversation instance is a QuerySet, not an Array, which makes it difficult to deal with when passing ti components.

Here's the solutions I've tried:

  1. Mapping the messages property of every Conversation model returned to an array of Messages with messages.all().toModelArray(). This gave me the error Can't mutate a reverse many-to-one relation, even when I tried cloning the object.

  2. Creating an entirely new plain old JavaScript object and copying all the properties over, then setting the correct value for messages. This worked, but creating all these new objects seems like a huge performance hog on an application with frequent state changes.

How should I achieve my goal here?


Solution

  • You ought to be able to do something akin to:

    return session.Conversation.all().toModelArray()
      .map(c => ({
        ...c.ref,
        messages: c.messages.toRefArray()
      }))
    

    in your selector. If that's not working, you might need to include more detail. You don't get relations for free with the initial toModelArray, you do need to 'query' them in order to use them in the selector.

    Normally I wouldn't just dump the entire store contents for these entities like this, I'd refine 'em to what the component actually requires:

    import { pick } from 'lodash/fp'
    
    // ...
    
    const refineConversation = c => ({
      ...pick([ 'id', 'updatedAt' ], c),
      messages: c.messages.toRefArray().map(refineMessages)
    })
    
    const refineMessages = m => ({
      ...pick([ 'id', 'author', 'text', 'updatedAt' ], m)
    })
    
    // ...
    
    return session.Conversation
      .all()
      .toModelArray()
      .map(refineConversation)
    

    While it can be tempting to just throw an object reference from the store at your component, there are a bunch of things on that object that your component probably doesn't need to render. If any of those things change, the component has to re-render and your selector can't use its memoised data.

    Remember that when you're using object spread (or Object.assign) you're creating shallow copies. Yes, there is a cost, but anything past the first level of nesting is using a reference so you're not cloning the whole thing. Use of selectors should protect you from having to do too much work in mapStateToProps (after the first render with that data).

    It's important to make good choices with state management, but the real performance hits are likely to come from other sources (unnecessary renders, waiting on API interaction, etc).