1

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?

Jon Gunter
  • 1,864
  • 2
  • 15
  • 21

1 Answers1

2

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).

Rich Churcher
  • 7,361
  • 3
  • 37
  • 60
  • I'm all against premature optimization, but wouldn't building all those new objects in my selectors be a huge performance drain? Especially if I add more foreign keys to each `Conversation` (maybe adding a specific `sender` object and an `attachments` one-to-many array). – Jon Gunter Dec 21 '17 at 15:52
  • 1
    Redux-ORM has a built-in customized version of Reselect's `createSelector`. The README has an example of using it: https://github.com/tommikaikkonen/redux-orm#use-with-react . That way, your "output selector" will only get recalculated when those "database tables" get updated. – markerikson Dec 21 '17 at 16:42
  • 2
    @markerikson is of course correct, and I see you're using it already. However I'll expand my answer to address performance in more detail. Short answer: don't worry about selector performance it... yet. – Rich Churcher Dec 21 '17 at 18:06