How to work with normalized data in TCA

Hey TCA Community,

I've been struggling a bit trying to figure out how to organize my state with TCA. I've banged my head against this for quite some time now, and haven't been able to come up with a good solution. I'd appreciate any insight/thoughts that y'all have.

As an example, I'd like you to imagine that we're building a chat application. In this application, we have Chats, Messages, Members.

A naive representation of the data, would look something like

struct Chat {
    var id: String
    var title: String?
    var messages: [Message]
    var members: [Member]
}

struct Message {
    var id: String
    var content: String
    var createdAt: Date
    var member: Member
}

struct Member {
    var id: String
    var name: String
}

My general heuristic for developing any kind of application is to first normalize everything. Applying this heuristic to my data, I would end up with something like

struct NormalizedChat {
    var id: String
    var title: String?
}

struct NormalizedMessage {
    var id: String
    var content: String
    var createdAt: Date
    
    var chatId: String
    var memberId: String
}

struct NormalizedMember {
    var id: String
    var name: String
}

struct Data {
    var chatsById: [String: NormalizedChat]
    var chatIds: [String]
    
    var messagesById: [String: NormalizedMessage]
    var messageIdsByChatId: [String: [String]]
    
    var membersById: [String: NormalizedMember]
    var memberIdsByChatId: [String: [String]]
}

My naive attempt to map this structure to TCA results in the following

struct ChatState {
    var chat: NormalizedChat
    var messages: [NormalizedMessage]
    var members: [NormalizedMember]
}

struct AppState {
    var data: Data
    
    var chats: [ChatState] {
        get {
            self.data.chatIds.compactMap { chatId in
                ChatState(
                    chat: self.data.chatsById[chatId],
                    messages: self.data.messageIdsByChatId[chatId].compactMap { self.data.messagesById[$0] },
                    members: self.data.memberIdsByChatId[chatId].compactMap { self.data.membersById[$0] }
                )
            }
        }
        
        set(newValue) {
            newValue.forEach { chatState in
                self.data.chatsById.updateValue(chatState.chat, forKey: chatState.chat.id)
                chatState.messages.forEach { message in
                    self.data.messagesById.updateValue(message, forKey: message.id)
                }
                
                chatState.members.forEach { member in
                    self.data.membersById.updateValue(member, forKey: member.id)
                }
            }
        }
    }
}

This mostly works just fine, but seems like a massive amount of effort to constantly be normalizing/de-normalizing the entire state tree each time any child changes.

For example, if I wanted to have a feature where a member could update their profile by navigating thru a chat, each keystroke would trigger the set for the ChatState.

Or, if I add a MessageComposerState to ChatState that holds the pending message state, I would (1) have to also store a normalized version of MessageComposerState and (2) each update to the MessageComposerState would trigger the set for ChatState.

  1. Is the example above the correct thing to do, or is there a better way to proceed? Is it as expensive as I am imagining?
  2. With TCA, it feels like, as soon as I start using computed properties for one piece of state, I have to pull all of that state up a layer. Is this thinking correct? Is there a better way to handle this?
  3. Is there a better way to structure my state? I've attempted a few other ways of structuring my data with clever pullbacks/scopes, but they always end up unclear and hard to reason about.

Any help/feedback would be greatly appreciated.

(also posted here How to work with normalized data with TCA · Discussion #717 · pointfreeco/swift-composable-architecture · GitHub)

2 Likes

While it doesn't address all of your issues, have you looked at IdentifiedArrayOf, which is part of Swift Identified Collections? It's a wrapper around an OrderedDictionary.

Terms of Service

Privacy Policy

Cookie Policy